02 加锁方法.txt

UP 返回
1 Synchronized原理与使用 (重量级锁。其他线程必须在外等待)
	1.1 代码写法
		内置锁			
		互斥锁
		
		修饰普通方法	内置锁就是当前类的实例
		修饰静态方法	内置锁是当前class字节码对象
		修饰代码块		内置锁可以是任意对象
	1.2 jvm层面的原理
		使用monitorenter	monitorexit指令来获取锁 释放锁 			(这是字节码文件中的内容)
		有的同步方法块中并没有这些指令,那是使用了别的同步机制,这里暂不表

  任何对象都可以作为锁,关于锁的信息存在对象头中。对象头含有这些信息:
	Mark Word		存储对象的hash值,锁等信息
	Class Metadata Address	指向对象类型的地址
	Array Length	如果是数组的话,数组对象要比普通对象多一个长度信息

  jdk1.6以后引入了其他几种锁:
	偏向锁		每次获取锁和释放锁会浪费资源。很多情况下,竞争锁不是由多个线程,而是由一个线程在使用。在只有一个线程访问同步代码块的场景下效率会更高
		偏向锁的Mark Word中会含有这些字段:
			线程id
			Epoch
			对象的分代年龄信息
			是否是偏向锁
			锁标志位
		线程第一次进来时,会查看锁标志,如果是偏向锁,再查看线程id。如果线程相同,则不再获取锁,直接进入,否则才会获取锁。线程第二次进来时其实是不用获取也不释放锁的;只有其他线程过来尝试获取锁时才会释放锁(等到竞争出现才释放的机制)
	轻量级锁		(不是很理解未做笔记,参视频15)
	重量级锁

2 单例模式与线程安全性问题
	饿汉式		没有线程安全性问题
		public class Singleton {
			private Singleton() {}
			private static Singleton singleton = new Singleton();
			public static Singleton getInstance() {
				return singleton;			//原子操作,多线程不会有问题
			}
		}
	懒汉式		双重检查加锁解决线程安全性问题			(饿汉式不论有没有使用都会创建对象,会更多的消耗资源)
		public class Singleton2 {
			private Singleton2() {}
			private static Singleton2 singleton;
			public static Singleton2 getInstance() 
				if (singleton == null) {					//非原子操作
					return new Singleton2();
				}
				return singleton;
			}
		}
		如果改成:
			public static synchronized Singleton2 getInstance() {		//在这里加同步会浪费很大的性能,不论是改为偏向锁还是轻量级锁都没多大意义(轻量级锁会在进入同步块后检查是否有线程正在执行,有的话会发生自旋浪费性能。参视频16)
				if (singleton == null) {
					return new Singleton2();
				}
				return singleton;
			}
		需要改成双重检查加锁:
			public static Singleton2 getInstance() {
				if (singleton == null) {
					synchronized (Singleton2.class) {
						if (singleton == null) {
							singleton = new Singleton2();		//在这个地方,可能会发生指令重排序的问题,导致达不到预期的效果。
																//事实上这句指令发生的行为为:1.申请一块内存空间 2.在这块空间中实例化对象 3.引用指向该空间
																//虚拟机可能会对这三条指令重排序,导致单例的实现出现问题
																//所以这个地方还需要将变量变为volatile : private static volatile Singleton2 singleton; 这个关键字加上以后就不会发生指令重排序等优化行为
						}
					}
				}
				return singleton;
			}

3 关于锁的相关概念	(参视频17)
  3.1 锁重入
	需要同一个锁对象的方法之间的相互调用。调用方拿到锁以后,被调用方可以直接进入。避免了死锁问题
  3.2 自旋锁
	相当于线程拿到锁以后一直等待,类似于while(true),直到被唤醒(轻量级锁中)
  3.3 死锁

4 volatile	(参视频18)
	Volatile称之为轻量级锁,被volatile修饰的变量,在线程之间是可见的。
		可见:一个线程修改了这个变量的值,在另外一个线程中能够读到这个修改后的值。
	Synchronized除了线程之间互斥意外,还有一个非常大的作用,就是保证可见性

	添加了volatile修饰的变量,编译汇编以后会多一条lock指令。lock指令的效果是:在多处理器的系统上,将当前处理器缓存行(CPU缓存的最小单位)的内容写回到系统内存;同时这个写回到内存的操作会使在其他CPU里缓存了该内存地址的数据失效
	volatile只能保证可见性,但是并不能保证原子操作;synchronized可以保证原子操作。所以如果同步的代码本身就是原子性的,就可以用volatile,更轻量。大量的使用volatile会使缓存无效,以及优化无效,所以不能多用
		
5 JDK提供的原子类原理及使用	(参视频19)
  在util.concurrent.atomic包下提供了多个原子类:
	原子更新基本类型		AtomicInteger
	原子更新数组		AtomicIntegerArray
	原子更新抽象类型		AtomicReference<V>
	原子更新字段	AtomicIntegerFieldUpdater<User>
  代码演示:
	public class AtomicTest {
	
		private AtomicInteger value = new AtomicInteger(0);// 原子基础类型
	
		private int[] s = { 1, 5, 6, 8 };
		AtomicIntegerArray array = new AtomicIntegerArray(s);// 原子数组
	
		AtomicReference<User> user = new AtomicReference<>();// 原子引用类型
	
		AtomicIntegerFieldUpdater<User> old = AtomicIntegerFieldUpdater.newUpdater(User.class, "old");// 原子更新字段
	
		public int getNext() {
			array.getAndIncrement(2);// 给数组某个位置值自增
			array.getAndAdd(2, 10);// 给数组某个位置值加上10
	
			User user = new User();
			old.getAndIncrement(user);
	
			return value.getAndIncrement();// 相当于自加操作
		}
	
		public static void main(String[] args) {
	
			AtomicTest atomicTest = new AtomicTest();
			new Thread(new Runnable() {
	
				@Override
				public void run() {
					// TODO Auto-generated method stub
					while (true) {
						System.out.println(atomicTest.getNext());
						try {
							Thread.sleep(500);
						} catch (Exception e) {
							// TODO: handle exception
						}
					}
				}
			}).start();
	
		}
	}
  User类
	public class User {
		private String name;
	
		public volatile int old;		//设置成public volatile才能执行AtomicIntegerFieldUpdater.newUpdater(User.class, "old")
	
		public String getName() {
			return name;
		}
	
		public void setName(String name) {
			this.name = name;
		}
	
		public int getOld() {
			return old;
		}
	
		public void setOld(int old) {
			this.old = old;
		}
	}

  实现原理:首先获得当前值prev和预期的下一个值next,再根据cas原子性操作判断是否只有本线程对其做修改,如果是则返回修改过的值,如果不是则循环执行判断
	public final int getAndUpdate(IntUnaryOperator updateFunction) {
	        int prev, next;
	        do {
	            prev = get();
	            next = updateFunction.applyAsInt(prev);
	        } while (!compareAndSet(prev, next));		//该操作将next赋给prev的同时会判断是否仅该线程对变量做了修改
	        return prev;
	    }
	
6 Lock接口		(参视频20)
  6.1 使用代码
	开始会出现并发问题的代码:
		public class LockTest {
			private int value;
		
			public int getNext() {
				return value++;
			}
		
			public static void main(String[] args) {
				LockTest lockTest = new LockTest();
				new Thread(new Runnable() {
					@Override
					public void run() {
						while (true) {
							System.out.println(Thread.currentThread().getName() + " " + lockTest.getNext());
							try {
								Thread.sleep(100);
							} catch (Exception e) {
								// TODO: handle exception
							}
						}
					}
				}).start();
				new Thread(new Runnable() {
					@Override
					public void run() {
						while (true) {
							System.out.println(Thread.currentThread().getName() + " " + lockTest.getNext());
							try {
								Thread.sleep(100);
							} catch (Exception e) {
								// TODO: handle exception
							}
						}
					}
				}).start();
			}
		}
	修改LockTest的代码:
		private int value;
		Lock lock = new ReentrantLock();
	
		public int getNext() {
			lock.lock();				//加锁
			int a = value++;
			lock.unlock();			//释放锁
			return a;
		}

  6.2 Lock需要显示地获取和释放锁,繁琐但能让代码更灵活。(比如再添加多个Lock对象,随时加锁释放锁)
	  Synchronized不需要显示地获取和释放锁,简单

	  使用Lock可以方便的实现公平性(Lock本身也是使用了Synchronized)

	  使用Lock的好处:
		非阻塞的获取锁
		能被中断的获取锁
		超时获取锁

  6.3 自己实现可重入锁:		(参视频21)
	测试类代码:
		public class Sequence {
		
			private MyLock myLock = new MyLock();
			private int value;
		
			public int getNext() {
				myLock.lock();
				value++;
				myLock.unlock();
				return value;
			}
		
			public static void main(String[] args) {
				Sequence sequence = new Sequence();
				new Thread(new Runnable() {
					@Override
					public void run() {
						// TODO Auto-generated method stub
						while (true) {
							System.out.println(sequence.getNext());
						}
					}
				}).start();
				new Thread(new Runnable() {
					@Override
					public void run() {
						// TODO Auto-generated method stub
						while (true) {
							System.out.println(sequence.getNext());
						}
					}
				}).start();
		
			}
		}
	锁代码:
		public class MyLock implements Lock {		//★此处省略其他无关的需要实现的方法
		
			private boolean isLocked = false;

			@Override
			public synchronized void lock() {
				// 自旋
				while (isLocked) {
					try {
						wait();
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				isLocked = true;
			}
		
			@Override
			public synchronized void unlock() {
				notify();
				isLocked = false;
			}
		}
	上述代码测试可以实现多线程获得的value可以不重复,但是无法解决可重入的问题,当用涉及到锁重入的测试类来进行时,会卡在a的打印上,因为方法b无法获得锁:
		public class Demo {
		
			private MyLock lock = new MyLock();
		
			public void a() {
				lock.lock();
				System.out.println("a");			//卡住
				b();									//无法获得锁
				lock.unlock();
			}
		
			public void b() {
				lock.lock();
				System.out.println("b");
				lock.unlock();
			}
		
			public static void main(String[] args) {
				Demo demo = new Demo();
				new Thread(new Runnable() {
		
					@Override
					public void run() {
						// TODO Auto-generated method stub
						demo.a();
					}
				}).start();
			}
		
		}
	这个时候需要对锁进行修改,记录加锁的线程并维护加锁的数量,同时释放锁时也需要比较,这样才能实现可重入:
		public class MyLock implements Lock {
		
			private boolean isLocked = false;
		
			Thread lockBy = null;					//★记录当前加锁的线程
			int lockCount = 0;						//★记录加锁数量
		
			@Override
			public synchronized void lock() {
				Thread currenThread = Thread.currentThread();
				// 自旋
				while (isLocked && currenThread != lockBy) {			//★如果当前锁是被占有的,同时占有的线程不是当前线程,则自旋等待
					try {
						wait();
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				isLocked = true;										//否则加锁,并记录加锁线程,加锁数量自增
				lockBy = currenThread;
				lockCount++;
			}

			@Override
			public synchronized void unlock() {
				if (lockBy == Thread.currentThread()) {				//解锁时,必须当前线程是加锁线程才有响应。如果条件成立,加锁数量自减
					lockCount--;
					if (lockCount == 0) {								//当加锁数量为0时锁释放,并通知其他线程
						notify();
						isLocked = false;
					}
				}
			}
		}

7 使用AQS重写自定义锁		(参视频23)
  7.1 测试类不变,锁代码更换:
	public class MyLock2 implements Lock { // 自己定义的锁
	
		private Helper helper = new Helper();
	
		// 根据帮助文档,应将具体的状态设置定义为锁的一个内部帮助类并继承AbstractQueuedSynchronizer
		// 同时重写tryAcquire和tryRelease
		private class Helper extends AbstractQueuedSynchronizer {
			@Override
			protected boolean tryAcquire(int arg) {
				// 第一个线程进来可以拿到锁,返回true
				// 第二个线程拿不到锁,返回false。但是如果是线程重入,则应该拿到锁,并更新状态值
				int state = getState();
				Thread t = Thread.currentThread();
				if (state == 0) {
					if (compareAndSetState(0, arg)) {// 保证原子操作
						setExclusiveOwnerThread(t);
						return true;
					}
				} else if (getExclusiveOwnerThread() == t) {// 重入
					setState(state + 1);
					return true;
				}
				return false;
			}
	
			@Override
			protected boolean tryRelease(int arg) {
				// 锁的获取和释放肯定是一一对应的,那么调用此方法的线程一定是当前线程
				if (Thread.currentThread() != getExclusiveOwnerThread()) {
					throw new RuntimeException();
				}
				int state = getState() - arg;
				boolean flag = false;
				if (getState() == 0) {
					setExclusiveOwnerThread(null);
					flag = true;
				}
				setState(state);
				return flag;
			}
	
			// 该方法是为了使用AbstractQueuedSynchronizer的内部类ConditionObject,方便锁newCondition接口的重写,暂时不管
			Condition newCondition() {
				return new ConditionObject();
			}
		}
	
		@Override
		public void lock() {
			helper.acquire(1);
		}
	
		@Override
		public void lockInterruptibly() throws InterruptedException {
			helper.acquireInterruptibly(1);
		}
	
		@Override
		public boolean tryLock() {
			return helper.tryAcquire(1);
		}
	
		@Override
		public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
			return helper.tryAcquireNanos(1, unit.toNanos(time));
		}
	
		@Override
		public void unlock() {
			helper.release(1);
		}
	
		@Override
		public Condition newCondition() {
			return helper.newCondition();
		}
	
	}


8 LongAdder		1.8出现(DoubleAdder同理)。详细代码解释参看视频56
  AtomicLong在多线程的表现性能很差,所以出现了LongAdder
 	 当多个线程争夺一个AtomicLong时自然会出现很多cas自旋降低性能,但是LongAdder使用一个base来保存基础值,当只有单线程访问时直接将这个值使用即可;当有多个线程访问,LongAdder会将base值拆分为多个小一点的值存放于一个cell数组中
  于是每一个线程就会抢占其中的每一个cell,这样就允许多线程同时处理;而这个cell数组的初始容量为2,一旦有线程抢占不到cell元素时,就会以*2的方式扩容
  获取具体的值的时候,是直接将整个cell数组的值求和返回,就保证了一致性。(比如加锁的值是5,执行+1操作时,将其拆分成2+3两个元素,对任意一个+1并不会影响最后得到的值)
	LongAdder中具体的cell细分方式来源于继承的Striped64,具体代码在这个类里可以看到
	cell数组存在一个伪共享的概念,视频并未具体说明

9 StampedLock	参视频57
  9.1 出现原因
	传统的读写锁,读和写是互斥的,有可能读线程非常多导致写线程一直抢占失败而饥饿(虽然公平锁可以防止饥饿,但是相对于非公平锁性能会更差)。StampedLock就是为了提高读写互斥导致的性能差,即StampedLock的读不会阻塞写,如果有写操作直接重新读。
	StampedLock在获取锁的时候可以拿到票据,根据票据是否变化即可知道读时是否发生了
  9.2 示例代码:
	public class Demo {
	
		private int balance;
	
		private StampedLock lock = new StampedLock();
	
		// 这种读锁和之前的没有区别
		public void read() {
			long stamp = lock.readLock();
			int c = balance;
			// 读出来后一系列操作...
			lock.unlockRead(stamp);
		}
	
		// 获取乐观锁
		public void optimisticRead() {
			long stamp = lock.tryOptimisticRead();
			int c = balance;
			// 这里可能会出现了写操作,因此要进行判断
			if (!lock.validate(stamp)) {
				// 要重新读
				long readStamp = lock.readLock();
				c = balance;
				// 更新票据,用于后面释放锁
				stamp = readStamp;
			}
			lock.unlockRead(stamp);
		}
	
		// 适用于读出一个值后进行判断,判断成功再写入
		public void conditionReadWrite(int value) {
			long stamp = lock.readLock();
			while (balance > 0) {// 假设用于更新的判断条件
				// 尝试转化为写锁
				long writeStamp = lock.tryConvertToWriteLock(stamp);
				if (writeStamp != 0) {// 转化成功,直接更新数据,完成操作
					stamp = writeStamp;
					balance += value;
					break;
				} else {// 没有转化成功,则直接释放读锁,然后获取写锁
					lock.unlockRead(stamp);
					stamp = lock.writeLock();
				}
			}
			lock.unlock(stamp);
		}
	
		public void write(int value) {
			long stamp = lock.writeLock();
			balance += value;
			lock.unlockWrite(stamp);
		}
	}
	
  9.3 为什么两种锁同时存在
	读写锁比StampedLock更加纯粹,获取锁释放锁;为了兼容以前广泛使用的读写锁

DOWN 返回