上锁:
public class SynchronizedThread implements Runnable{ private static int count; //构造器初始化count public SynchronizedThread() { count = 0; } @Override public void run() { synchronized (this){ for (int i = 0; i < 5; i++) { try { System.out.println(Thread.currentThread().getName()+":"+(count++)); Thread.sleep(1000); }catch (InterruptedException e){ e.printStackTrace(); } } } } }调用:
情况一:两个线程t1和t2拿的是不同的Runnable对象 public class Demo02 extends TestThread{ public static void main(String[] args) { SynchronizedThread s1 = new SynchronizedThread(); SynchronizedThread s2 = new SynchronizedThread(); Thread t1 = new Thread(s1); Thread t2 = new Thread(s2); t1.start(); t2.start(); } }运行结果:可以看出两个线程同时在执行,为什么?我们可以看到synchronized的作用范围,作用对象是方法的时候,锁住的是对象的实例(this),而我们的两个线程分别拿的并不是同一个对象,每个对象只有一个锁与之相关联。
Thread-0:0 Thread-1:1 Thread-0:2 Thread-1:3 Thread-0:4 Thread-1:4 Thread-0:5 Thread-1:6 Thread-1:7 Thread-0:7 情况二:针对情况一,我们对两个线程传入同一个Runnable对象 public class Demo02 extends TestThread{ public static void main(String[] args) { SynchronizedThread s = new SynchronizedThread(); Thread t1 = new Thread(s); Thread t2 = new Thread(s); t1.start(); t2.start(); } }运行结果:我们可以看到t1线程完成后,t2线程才可以开始。这是因为两个并发线程访问同一个对象的synchronized 代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。
Thread-0:0 Thread-0:1 Thread-0:2 Thread-0:3 Thread-0:4 Thread-1:5 Thread-1:6 Thread-1:7 Thread-1:8 Thread-1:9 银行存钱取钱的例子: 账户: public class Account { private String name; private double amount; public Account(String name, double amount) { this.name = name; this.amount = amount; } /** * 存钱 * @param money 存的钱 */ public void saveMoney(double money){ amount = amount + money; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 取钱 * @param money 取的钱 */ public void drawMoney(double money){ amount = amount - money; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 查看余额 * @return */ public double getAmount() { return amount; } } 账户操作类: public class AccountOperator implements Runnable{ private Account account; public AccountOperator(Account account) { this.account = account; } @Override public void run() { //一个用户账号存钱 取钱的时候, 同一个账号 在其他地方不能进行存钱取钱 synchronized (this){ //取钱 account.drawMoney(1000); //存钱 account.saveMoney(1500); System.out.println(Thread.currentThread().getName()+":"+account.getAmount()); } } } 测试 public class Demo { public static void main(String[] args) { //账户 Account account = new Account("小明",2500); //银行 AccountOperator operator = new AccountOperator(account); //多个银行对同一个账户进行存取钱操作 Thread t1 = new Thread(operator,"银行A"); Thread t2 = new Thread(operator,"银行B"); Thread t3 = new Thread(operator,"银行C"); t1.start(); t2.start(); t3.start(); } }运行结果:我们每次都是取1000,存1500,所以每次都会增加500
银行A:3000.0 银行C:3500.0 银行B:4000.0 两个并发线程访问同一个对象,线程A执行synchronized的方法时候,线程B只执行非synchronized的方法并不会受阻塞。具体用法:
public class SynchronizedThread implements Runnable{ private static int count; //构造器初始化count public SynchronizedThread() { count = 0; } public synchronized static void method(){ for (int i = 0; i < 5; i++) { try { System.out.println(Thread.currentThread().getName()+":"+(count++)); Thread.sleep(1000); }catch (InterruptedException e){ e.printStackTrace(); } } } @Override public synchronized void run() { method(); } }调用:
public class Demo02 extends TestThread{ public static void main(String[] args) { SynchronizedThread s1 = new SynchronizedThread(); SynchronizedThread s2 = new SynchronizedThread(); Thread t1 = new Thread(s1); Thread t2 = new Thread(s2); t1.start(); t2.start(); } }运行结果:我们可以看到两个线程是访问类SynchronizedThread的两个不同对象,但是线程2还是受阻塞了,原因就是线程1和线程2都是调用的synchronized修饰的静态方法,锁住的是这个SynchronizedThread类。
Thread-0:0 Thread-0:1 Thread-0:2 Thread-0:3 Thread-0:4 Thread-1:5 Thread-1:6 Thread-1:7 Thread-1:8 Thread-1:9具体使用:(同步线程)
public class SynchronizedThread implements Runnable{ private static int count; //构造器初始化count public SynchronizedThread() { count = 0; } public static void method(){ synchronized (SynchronizedThread.class){ for (int i = 0; i < 5; i++) { try { System.out.println(Thread.currentThread().getName()+":"+(count++)); Thread.sleep(1000); }catch (InterruptedException e){ e.printStackTrace(); } } } } @Override public synchronized void run() { method(); } }给class加锁和给静态方法加锁是一样的,所有对象公用一把锁。
在synchronized内部包括ContentionList、EntryList、WaitSet、OnDeck、Owner、! Owner这6个区域,每个区域的数据都代表锁的不同状态。
Wait Set:等待集合,哪些调用 wait 方法被阻塞的线程被放置在这里。Contention List:锁竞争队列,所有请求锁的线程首先被放在这个竞争队列中。Entry List:竞争候选列表,Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中。OnDeck:竞争候选者,在同一时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;Owner:当前已经获取到锁资源的线程被称为 Owner;!Owner:在Owner线程释放锁后,会从Owner的状态变成!Owner。synchronized实现:
synchronized在收到新的锁请求时首先自旋,如果通过自旋也没有获取锁资源,则将被放入锁竞争队列ContentionList中。
JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList 会被大量的并发线程进行 CAS (Compare And Swap,比较和交换)访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到 EntryList 中作为候选竞争线程。
Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。
Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM 中,也把这种选择行为称之为“竞争切换”。
OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify或者 notifyAll 唤醒,会重新进去 EntryList 中。
处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。
Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源。
每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的。
synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。
Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀。
JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁。
ReentrantLock有显式的操作过程,何时加锁、何时释放锁都在程序员的控制之下。具体的使用流程是定义一个ReentrantLock,在需要加锁的地方通过lock方法加锁,等资源使用完成后再通过unlock方法释放锁。
ReentrantLock使用: 1)lock()必须紧跟try代码块,且unlock()要放到finally第一行。 2)ReentrantLock锁可以反复进入,允许连续两次获得同一把锁,两次释放同一把锁。 3)获取锁和释放锁的次数要相同,如果释放锁的次数多于获取锁的次数,Java就会抛出java.lang.IllegalMonitorStateException异常;如果释放锁的次数少于获取锁的次数,该线程就会一直持有该锁,其他线程将无法获取锁资源。
//step2:定义一一个ReentrantLock public static ReentrantLock lock = new ReentrantLock(); ......... //step2:上锁 lock.lock(); //lock.lock(); 可重入锁 try { i++; }finally { lock.unlock();//step3:释放锁 //lock.unlock();可重入锁 }具体代码示例:
public class ReentrantLockDemo implements Runnable{ //Step1: public static ReentrantLock lock = new ReentrantLock(); public static int i = 0; @Override public void run() { for (int j = 0; j < 10; j++) { lock.lock(); //lock.lock(); 可重入锁 try { i++; }finally { lock.unlock(); //lock.unlock();可重入锁 } } } public static void main(String[] args) throws InterruptedException { ReentrantLockDemo reentrantLock = new ReentrantLockDemo(); Thread thread = new Thread(reentrantLock); thread.start(); thread.join(); System.out.println(i); } }在synchronized中如果有一个线程尝试获取一把锁,则其结果是要么获取锁继续执行,要么保持等待。ReentrantLock还提供了可响应中断的可能,即在等待锁的过程中,线程可以根据需要取消对锁的请求。
代码测试:
定义两把锁lock1和lock2,两个线程thread1和thread2,thread1先占用lock1,再占用lock2; thread2则先占用lock2,后占用lock1,这便形成了thread1和thread2之间的相互等待,在两个线程都启动时便处于死锁状态。我们就可以采用响应中断来解决这个问题,设置一个等待时间,如果超过这个时间,设置某个线程主动中断,释放对lock的申请,同时释放以获得的lock,让其他线程获得lock继续执行下去。代码示例中设置了,如果等待时间过长,等待时间设置为3s,thread2就会主动interrupt,释放对lock1的申请和以获得的lock2,让thread1获得lock2,继续执行。 public class InterruptResponse { //Step1: 定义两把锁 lock1和lock2 public ReentrantLock lock1 = new ReentrantLock(); public ReentrantLock lock2 = new ReentrantLock(); //Step2: 定义两个线程 thread1和thread2 Thread thread1 = new Thread(new Runnable() { @Override public void run() { try { //lockInterruptibly() :如果当前线程未被中断,则获得锁 lock1.lockInterruptibly(); try { //逻辑操作 Thread.sleep(500); }catch (InterruptedException e){ e.printStackTrace(); } lock2.lockInterruptibly(); System.out.println(Thread.currentThread().getName()+"执行完毕"); } catch (InterruptedException e) { e.printStackTrace(); }finally { //isHeldByCurrentThread():业务逻辑执行完后,检查当前线程释放持有该锁,有的话释放 if (lock1.isHeldByCurrentThread()){ lock1.unlock(); } if (lock2.isHeldByCurrentThread()){ lock2.unlock(); } System.out.println(Thread.currentThread().getName()+"退出"); } } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { try { //lockInterruptibly() :如果当前线程未被中断,则获得锁 lock2.lockInterruptibly(); try { //逻辑操作 Thread.sleep(500); }catch (InterruptedException e){ e.printStackTrace(); } lock1.lockInterruptibly(); System.out.println(Thread.currentThread().getName()+"执行完毕"); } catch (InterruptedException e) { e.printStackTrace(); }finally { //isHeldByCurrentThread():业务逻辑执行完后,检查当前线程释放持有该锁,有的话释放 if (lock1.isHeldByCurrentThread()){ lock1.unlock(); } if (lock2.isHeldByCurrentThread()){ lock2.unlock(); } System.out.println(Thread.currentThread().getName()+"退出"); } } }); public static void main(String[] args) { long time = System.currentTimeMillis(); InterruptResponse response = new InterruptResponse(); response.thread1.start(); response.thread2.start(); //自旋一段时间,如果等待时间过长,则会发生死锁等问题,主动中断并释放锁 while (true){ if (System.currentTimeMillis() - time >= 3000){ response.thread2.interrupt(); } } } }运行结果:
java.lang.InterruptedException at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222) at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335) at test.gaolang.reentrant.InterruptResponse$2.run(InterruptResponse.java:63) at java.lang.Thread.run(Thread.java:748) Thread-0执行完毕 Thread-1退出 Thread-0退出通过boolean tryLock()获取锁。如果有可用锁,则获取该锁并返回true,如果无可用锁,则立即返回false。
boolean b = lock2.tryLock();通过boolean tryLock(long time, TimeUnit unit) throws InterruptedException获取定时锁。如果在给定的时间内获取到了可用锁,且当前线程未被中断,则获取该锁并返回true。如果在给定的时间内获取不到可用锁,将禁用当前线程,并且在发生以下三种情况之前,该线程一直处于休眠状态。
当前线程获取到了可用锁并返回true。当前线程在进入此方法时设置了该线程的中断状态,或者当前线程在获取锁时被中断,则将抛出InterruptedException,并清除当前线程的已中断状态。当前线程获取锁的时间超过了指定的等待时间,则将返回false。如果设定的时间小于等于0,则该方法将完全不等待。公平锁: 加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得
非公平锁: 加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列Java 中的 synchronized 是非公平锁,ReentrantLock 默认的 lock()方法采用的是非公平锁。 因为公平锁需要在多核的情况下维护一个锁线程等待队列,基于该队列进行锁的分配,因此效率比非公平锁低很多。Java中的synchronized是非公平锁,ReentrantLock默认的lock方法采用的是非公平锁。独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。 独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线 程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
独占锁共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。 1)AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列中等待线程的锁获取模式。 2) java 的并发包中提供了 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个 写操作访问,但两者不能同时进行。
为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。
读锁 如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁写锁 如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!Java 中读写锁有个接口 java.util.concurrent.locks.ReadWriteLock ,也有具体的实现ReentrantReadWriteLock。代码实现: public class ReaderWriter { private final Map<String,Object> cache = new HashMap<String,Object>(); private final ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock(); //定义读写锁 private final Lock readLock = rwlock.readLock(); private final Lock writeLock = rwlock.writeLock(); //在读数据时,加读锁 public Object get(String key){ readLock.lock(); try { return cache.get(key); }finally { readLock.unlock(); } } //写数据时加写锁 public Object put(String key,Object value){ writeLock.lock(); try { return cache.put(key,value); }finally { writeLock.unlock(); } } } ReentrantReadWriteLock的加锁和解锁操作最终都调用内部类Sync提供的方法。Sync对象通过继承AQS(Abstract Queued Synchronizer)进行实现。AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,分别标识AQS队列中等待线程的锁获取模式。除了在多线程之间存在竞争获取锁的情况,还会经常出现同一个锁被同一个线程多次获取的情况。偏向锁用于在某个线程获取某个锁之后,消除这个线程锁重入的开销,看起来似乎是这个线程得到了该锁的偏向(偏袒)。
偏向锁的主要目的是在同一个线程多次获取某个锁的情况下尽量减少轻量级锁的执行路径,因为轻量级锁的获取及释放需要多次CAS(Compare and Swap)原子操作,而偏向锁只需要在切换ThreadID时执行一次CAS原子操作,因此可以提高锁的运行效率。
在出现多线程竞争锁的情况时,JVM会自动撤销偏向锁,因此偏向锁的撤销操作的耗时必须少于节省下来的CAS原子操作的耗时。
轻量级锁用于提高线程交替执行同步块时的性能,偏向锁则在某个线程交替执行同步块时进一步提高性能。
锁的状态总共有4种:无锁、偏向锁、轻量级锁和重量级锁。随着锁竞争越来越激烈,锁可能从偏向锁升级到轻量级锁,再升级到重量级锁,但在Java中锁只单向升级,不会降级。
分段锁并非一种实际的锁,而是一种思想,用于将数据分段并在每个分段上都单独加锁,把锁进一步细粒度化,以提高并发效率。ConcurrentHashMap在内部就是使用分段锁实现的。
减少锁持有时间 只用在有线程安全要求的程序上加锁来尽量减少同步代码块对锁的持有时间。
减小锁粒度 将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是ConcurrentHashMap。
锁分离
锁分离指根据不同的应用场景将锁的功能进行分离,以应对不同的变化,最常见的锁分离思想就是读写锁(ReadWriteLock),它根据锁的功能将锁分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,既保证了线程的安全性,又提高了性能。
操作分离思想可以进一步延伸为只要操作互不影响,就可以进一步拆分,比如LinkedBlockingQueue从头部取出数据,并从尾部加入数据。
锁粗化 通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。锁消除 锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这 些对象的锁操作,多数是因为程序员编码不规范引起。Semaphore是一种基于计数的信号量,在定义信号量对象时可以设定一个阈值,基于该阈值,多个线程竞争获取许可信号,线程竞争到许可信号后开始执行具体的业务逻辑,业务逻辑在执行完成后释放该许可信号。在许可信号的竞争队列超过阈值后,新加入的申请许可信号的线程将被阻塞,直到有其他许可信号被释放。Semaphore 可以用来构建一些对象池,资源池之类的,比如数据库连接池。
Semaphore的基本用法:
//1:创建一个计数阈值为 5 的信号量对象:只能有5个线程同时访问 Semaphore semaphore = new Semaphore(5); try { //2:申请许可 semaphore.acquire(); try { //3:业务逻辑代码 }catch (Exception e){ }finally { //4:释放许可 semaphore.release(); } }catch (InterruptedException e){ } Semaphore对锁的申请和释放和ReentrantLock类似,通过acquire方法和release方法来获取和释放许可信号资源。Semaphone.acquire方法默认和ReentrantLock. lockInterruptibly方法的效果一样,为可响应中断锁,也就是说在等待许可信号资源的过程中可以被Thread.interrupt方法中断而取消对许可信号的申请。Semaphore 也实现了可轮询的锁请求与定时锁的功能,除了方法名 tryAcquire 与 tryLock不同,其使用方法与 ReentrantLock 几乎一致。Semaphore 也提供了公平与非公平锁的机制,也可在构造函数中进行设定。Semaphore的锁释放操作也需要手动执行,因此,为了避免线程因执行异常而无法正常释放锁,释放锁的操作必须在finally代码块中完成。运行结果:2000000
原子操作即是进行过程中不能被中断的操作,针对某个值的原子操作在被进行的过程中,CPU绝不会再去进行其他的针对该值的操作。为了实现这样的严谨性,原子操作仅会由一个独立的CPU指令代表和完成。原子操作是无锁的,常常直接通过CPU指令直接实现。