企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] # 线程同步的几种方法 1. 用什么关键字修饰同步方法 ? 用synchronized关键字修饰同步方法 2. 同步有几种实现方法,都是什么?分别是synchronized,wait与notify ~~~ wait():使一个线程处于等待状态,并且释放所持有的对象的lock。 sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。 notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。 notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。 ~~~ 3. 特殊域变量,volatile,注意不能修饰final的变量. a. volatile关键字为域变量的访问提供了一种免锁机制 b. 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新 c. 因此每次使用该域就要重新计算,而不是使用寄存器中的值 d. volatile不会提供任何原子操作,它也不能用来修饰final类型的变量 4. 使用重入锁ReentrantLock 在JavaSE5.0中新增了一个Java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁. 5. 使用局部变量ThreadLocal实现 # synchronized synchronized是用来实现线程同步的!!! ~~~ 加同步格式: synchronized( 需要一个任意的对象(锁) ){ 代码块中放操作共享数据的代码。 } ~~~ ## synchronized的缺陷 synchronized是java中的一个关键字,也就是说是Java语言内置的特性。 如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况: 1. 获取锁的线程执行完了该代码块,然后线程释放对锁的占有; 2. 线程执行发生异常,此时JVM会让线程自动释放锁。 例子1:   如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。   因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。 例子2: 当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。   但是采用synchronized关键字来实现同步的话,就会导致一个问题: 如果多个线程都只是进行读操作,当一个线程在进行读操作时,其他线程只能等待无法进行读操作。   因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。   另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。   总的来说,也就是说Lock提供了比synchronized更多的功能。 # lock ## lock和synchronized的区别   1. Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;   2. Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。 java.util.concurrent.locks包下常用的类Lock 首先要说明的就是Lock,通过查看Lock的源码可知,Lock是一个接口: ~~~ public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); } ~~~ 下面来逐个讲述Lock接口中每个方法的使用,lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。newCondition()这个方法暂且不在此讲述,会在后面的线程协作一文中讲述。 在Lock中声明了四个方法来获取锁,那么这四个方法有何区别呢? ## lock 首先lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。 由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的: ~~~ Lock lock = ...; lock.lock(); try{ //处理任务 }catch(Exception ex){ }finally{ lock.unlock(); //释放锁 } ~~~ ## tryLock tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。 tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。 所以,一般情况下通过tryLock来获取锁时是这样使用的: ~~~ Lock lock = ...; if(lock.tryLock()) { try{ //处理任务 }catch(Exception ex){ }finally{ lock.unlock(); //释放锁 } }else { //如果不能获取锁,则直接做其他事情 } ~~~ ## lockInterruptibly lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。 由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。 因此lockInterruptibly()一般的使用形式如下: ~~~ public void method() throws InterruptedException { lock.lockInterruptibly(); try { //..... } finally { lock.unlock(); } } ~~~ 注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。 因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。 而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。 # Lock和synchronized的选择 总结来说,Lock和synchronized有以下几点不同: 1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现; 2. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁; 3. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断; 4. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。 5. Lock可以提高多个线程进行读操作的效率。 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。 # ReentrantLock ## lock用法 ReentrantLock,意思是“可重入锁”,关于可重入锁的概念在下一节讲述。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。下面通过一些实例看具体看一下如何使用ReentrantLock。 例子1,lock()的正确使用方法 ~~~ package testThread; import java.util.ArrayList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class TestThread { private ArrayList<Integer> arrayList = new ArrayList<Integer>(); public static void main(String[] args) { final TestThread testThread = new TestThread(); new Thread() { public void run() { testThread.insert(Thread.currentThread()); } }.start(); new Thread() { public void run() { testThread.insert(Thread.currentThread()); } }.start(); } private void insert(Thread thread) { //注意这个地方 Lock lock = new ReentrantLock(); lock.lock(); try { System.out.println(thread.getName() + "得到了锁"); for (int i = 0; i < 5; i++) { arrayList.add(i); } } catch (Exception e) { e.printStackTrace(); } finally { System.out.println(thread.getName() + "释放锁"); lock.unlock(); } } } ~~~ 各位朋友先想一下这段代码的输出结果是什么? ~~~ Thread-0得到了锁 Thread-1得到了锁 Thread-1释放锁 Thread-0释放锁 ~~~ 也许有朋友会问,怎么会输出这个结果?第二个线程怎么会在第一个线程释放锁之前得到了锁?原因在于,在insert方法中的lock变量是局部变量,每个线程执行该方法时都会保存一个副本,那么理所当然每个线程执行到lock.lock()处获取的是不同的锁,所以就不会发生冲突。 知道了原因改起来就比较容易了,只需要将lock声明为类的属性即可 ~~~ package testThread; import java.util.ArrayList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class TestThread { private ArrayList<Integer> arrayList = new ArrayList<Integer>(); private Lock lock = new ReentrantLock(); public static void main(String[] args) { final TestThread testThread = new TestThread(); new Thread() { public void run() { testThread.insert(Thread.currentThread()); } }.start(); new Thread() { public void run() { testThread.insert(Thread.currentThread()); } }.start(); } private void insert(Thread thread) { lock.lock(); try { System.out.println(thread.getName() + "得到了锁"); for (int i = 0; i < 5; i++) { arrayList.add(i); } } catch (Exception e) { e.printStackTrace(); } finally { System.out.println(thread.getName() + "释放锁"); lock.unlock(); } } } ~~~ 这样就是正确地使用Lock的方法了 ## tryLock ~~~ package testThread; import com.sun.org.apache.bcel.internal.generic.IF_ACMPEQ; import java.util.ArrayList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class TestThread { private ArrayList<Integer> arrayList = new ArrayList<Integer>(); private Lock lock = new ReentrantLock(); public static void main(String[] args) { final TestThread testThread = new TestThread(); new Thread() { public void run() { testThread.insert(Thread.currentThread()); } }.start(); new Thread() { public void run() { testThread.insert(Thread.currentThread()); } }.start(); } private void insert(Thread thread) { if (lock.tryLock()) { try { System.out.println(thread.getName() + "得到了锁"); for (int i = 0; i < 5; i++) { arrayList.add(i); } } catch (Exception e) { e.printStackTrace(); } finally { System.out.println(thread.getName() + "释放锁"); lock.unlock(); } } else { System.out.println(thread.getName() + "获取锁失败"); } } } ~~~ 输出结果 ~~~ Thread-0得到了锁 Thread-0释放锁 Thread-1得到了锁 Thread-1释放锁 ~~~ ## lockInterruptibly lockInterruptibly()响应中断的使用方法: ~~~ public class Test { private Lock lock = new ReentrantLock(); public static void main(String[] args) { Test test = new Test(); MyThread thread1 = new MyThread(test); MyThread thread2 = new MyThread(test); thread1.start(); thread2.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //调用中断方法来测试能否中断等待中的线程 thread2.interrupt(); } public void insert(Thread thread) throws InterruptedException{ lock.lockInterruptibly(); //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出 try { System.out.println(thread.getName()+"得到了锁"); long startTime = System.currentTimeMillis(); for( ; ;) { if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE) break; //插入数据 } } finally { System.out.println(Thread.currentThread().getName()+"执行finally"); lock.unlock(); System.out.println(thread.getName()+"释放了锁"); } } } class MyThread extends Thread { private Test test = null; public MyThread(Test test) { this.test = test; } @Override public void run() { try { test.insert(Thread.currentThread()); } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName()+"被中断"); } } } ~~~ 运行之后,发现thread2能够被正确中断 ## ReadWriteLock ReadWriteLock也是一个接口,在它里面只定义了两个方法