多线程学习总结
一、线程简介
1、概念
线程是进程中的独立的运行个体,也是最小的运行单位,进程是资源分配的基本单位,一个进程中同时运行多个线程为多线程。使用多线程可以节约CPU资源。
2、生命周期
- 新建 :
从新建一个线程对象到程序start() 这个线程之间的状态,都是新建状态; - 就绪 :
线程对象调用start()方法后,就处于就绪状态,等到JVM里的线程调度器的调度; - 运行 :
就绪状态下的线程在获取CPU资源后就可以执行run(),此时的线程便处于运行状态,运行状态的线程可变为就绪、阻塞及死亡三种状态。 - 等待/阻塞/睡眠 :
在一个线程执行了sleep(睡眠)、suspend(挂起)等方法后会失去所占有的资源,从而进入阻塞状态,在睡眠结束后可重新进入就绪状态。 - 终止 :
run()方法完成后或发生其他终止条件时就会切换到终止状态。
线程开始时:就绪状态,等待cpu调用后进入运行状态,运行过程中遇到阻塞事件,进入阻塞状态,等待阻塞事件结束后,重新进入就绪状态;如果没有阻塞事件,运行结束后,则进入结束状态。
3、线程创建
1.继承Thread类
public class ToExtendThread extends Thread{
@Override
public void run() {
System.out.println("通过继承Thread类:"+Thread.currentThread().getName());
}
public static void main(String[] args) {
//创建线程对象
ToExtendThread thread1 = new ToExtendThread();
ToExtendThread thread2 = new ToExtendThread();
//启动线程
thread1.start();
thread2.start();
}
}
2.实现Runnable接口
public class ToImpRunable implements Runnable{
//覆盖run方法
@Override
public void run() {
System.out.println("通过实现Runnable方法:"+Thread.currentThread().getName());
}
public static void main(String[] args) {
ToImpRunable runable = new ToImpRunable();
//创建线程对象
Thread thread1 = new Thread(runable);
Thread thread2 = new Thread(runable);
//启动线程
thread1.start();
thread2.start();
}
}
3.实现Callable接口
public class ToImpCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("通过实现Callable创建线程");
int i=1;
return i;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建callable对象
ToImpCallable callable = new ToImpCallable();
//通过FutureTask封装FutureTask对象
FutureTask<Integer> futureTask = new FutureTask<>(callable);
//创建线程对象
Thread thread = new Thread(futureTask);
thread.start();
Integer integer = null;
//通过FutureTask的get方法获取call方法中的返回值
integer = futureTask.get();
System.out.println(integer);
}
}
4.创建线程池
1.自定义线程池
自定义线程池ThreadPoolExecutor有五个参数:
- corePoolSize
表示线程池核心线程数,当初始化线程池时,会创建核心线程进入等待状态,即使它是空闲的,核心线程也不会被摧毁,从而降低了任务一来时要创建新线程的时间和性能开销。 - maximumPoolSize
表示最大线程数,意味着核心线程数都被用完了,那只能重新创建新的线程来执行任务,但是前提是不能超过最大线程数量,否则该任务只能进入阻塞队列进行排队等候,直到有线程空闲了,才能继续执行任务。 - keepAliveTime
表示非核心线程存活时间,除了核心线程外,那些被新创建出来的线程可以存活多久。意味着,这些新的线程一但完成任务,而后面都是空闲状态时,就会在一定时间后被摧毁。 - unit
存活时间单位,即使非核心线程存活时间的单位。 - workQueue
表示任务的阻塞队列,由于任务可能会有很多,而线程就那么几个,所以那么还未被执行的任务就进入队列中排队,队列我们知道是 FIFO 的,等到线程空闲了,就从阻塞队列中取出任务。
此外还有两个系统默认的参数:
- threadFactory
线程工厂,用于创建线程,一般用默认的即可 - handler
线程池对拒绝任务的处理策略
阻塞队列workQueue就是当队列为空或满时,获取元素或存储元素的线程会等待,常用于生产者消费者场景。
常用的有以下7个阻塞队列:
- ArrayBlockingQueue
一个由数组结构组成的有界阻塞队列,按照先进先出进行排序。可以设置队列容量和是否是公平锁,公平锁即先进队列的先执行,使用可重入锁ReentrantLock实现,但这样效率会降低,默认不保证公平,即争抢式。 - LinkedBlockingQueue
一个由链表结构组成的有界阻塞队列,默认最大值为Integer.MAX_VALUE,也是按照先进先出排序。 - PriorityBlockingQueue
一个支持优先级排序的无界阻塞队列。默认使用升序排列。可以通过comparator 比较器设置排序规则。 - DelayQueue
一个使用优先级队列实现的无界阻塞队列。可以支持延时获取元素。队列使用 PriorityQueue 来实现。队列中的元素必须实现 Delayed 接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。可以应用于定时任务调度。 - SynchronousQueue
一个不存储元素的阻塞队列。每一个put 操作必须等待一个 take 操作,否则不能继续添加元素。使用于线程间数据交互频繁的场景。 比如在一个线程中使用的数据,传递给另外一个线程使用,SynchronousQueue 的吞吐量高于 LinkedBlockingQueue 和 ArrayBlockingQueue。 - LinkedTransferQueue
一个由链表结构组成的无界阻塞队列。新增了transfer 方法。如果当前有消费者正在等待接收元素(消费者使用 take() 方法或带时间限制的 poll() 方法时),transfer 方法可以把生产者传入的元素立刻 transfer(传输)给消费者,不经过阻塞队列。如果没有消费者在等待接收元素,transfer 方法会将元素存放在队列的 tail 节点,并等到该元素被消费者消费了才返回。 - LinkedBlockingDeque
一个由链表结构组成的双向阻塞队列。可以从队列两端添加和移除元素。
ThreadPoolExecutor提供了四种handler拒绝任务策略:
- AbortPolicy:丢弃任务并抛出-RejectedExecutionException异常;也是默认的处理方式。
- DiscardPolicy:丢弃任务,但是不抛出异常。
- DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
- CallerRunsPolicy:由调用线程处理该任务
2.newFixedThreadPool
创建一个定长线程池,核心线程数等于最大线程数。,而且它们的线程数存活时间都是无限的。可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置,线程池参数如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
3.newSingleThreadExecutor
创建一个单线程化的线程池,他的最大数为1,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(如FIFO,LIFO,优先级)执行,线程池参数如下:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
4.newScheduledThreadPool
创建一个定长线程池,支持定时及周期性执行任务,内部有一个延时队列来维护任务进行时间。线程池参数如下:
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
5.newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无回收,则新建线程,意味着没有核心线程,每次需要线程时直接创建,然后缓存一段时间,内部维护一个SynchronousQueue队列。线程池参数如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
6.newWorkStealingPool
可以根据CPU的核数并行的执行,适合使用在很耗时的操作,充分利用CPU执行任务。任务窃取线程池,不保证执行顺序,适合任务耗时差异较大,线程中有的线程队列任务堆积,有的线程队列为空,就存在有的线程处于饥饿状态,当一个线程处于饥饿状态时,它就会去其他线程队列中窃取任务,解决饥饿导致的效率问题,内部有一个ForkJoinPool并行线程池,进行抢占式工作,线程池参数如下:
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
线程池的调用如下:
public class ThreadPoolExecutorDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//线程池参数分别是:核心线程数、最大线程池数、超过核心线程数的空闲存活时间、时间的单位、阻塞队列
ExecutorService executor1=new ThreadPoolExecutor(
5,
10,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10)
);
//创建单核心的线程池
ExecutorService executor2 = Executors.newSingleThreadExecutor();
//创建一个按照定时计划规定执行的线程池
ExecutorService executor3 = Executors.newScheduledThreadPool(4);
//创建一个自动增长的线程池
ExecutorService executor4 = Executors.newCachedThreadPool();
//创建固定核心数的线程池
ExecutorService executor5 = Executors.newFixedThreadPool(5);
//创建一个具有抢占式操作的线程池
ExecutorService executor6 = Executors.newWorkStealingPool();
ToExtendThread thread = new ToExtendThread();
ToImpRunable runable = new ToImpRunable();
ToImpCallable callable = new ToImpCallable();
//submit方法有返回值,execute方法没有
executor1.execute(thread);
executor1.execute(runable);
Future<Integer> submit = executor1.submit(callable);
Integer integer = submit.get();
System.out.println(integer);
}
}
4、线程常用方法
优活强睡,礼中等唤。
-
Thread.currentThead():
获取当前线程对象 -
getPriority()
获取当前线程的优先级 -
setPriority()
设置当前线程的优先级,线程优先级高,被CPU调度的概率大 -
isAlive()
判断线程是否处于活动状态 (线程调用start后,即处于活动状态) -
join()
调用t.join方法的线程强制执行,join方法只会使主线程进入阻塞状态并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程。 -
sleep()
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。休眠的线程进入阻塞状态,不会释放对象的锁。 -
yield()
调用yield方法的线程,会礼让其他线程先运行。(大概率其他线程先运行,小概率自己还会运行) -
interrupt()
中断线程,只是给定一个中断状态,何时中断由线程自己决定。isinterrupted判断是否有中断状态。可以多次判断,interrupted判断后就移除中断状态。如果线程处于sleep, wait, join 等状态,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常; -
wait()
导致线程等待,进入堵塞状态。它必须包含在Synchronzied语句中 -
notify()
唤醒当前线程,进入运行状态。它必须包含在Synchronzied语句中 -
notifyAll()
唤醒所有等待的线程。它必须包含在Synchronzied语句中
二、多线程同步
1、线程安全
当多个线程同时访问某个类、对象集合等时,不论线程间的调用方式,这个类中被访问的方法等都能正确运行,那么这个类是线程安全的。Java内存模型如下:
当线程执行i++时候,线程的工作内存运行如下:
线程安全有三个要素:
- 原子性
对共享内存的操作必须是要么全部执行直到执行结束,且中间过程不能被线程调度器打断,要么就不执行。提供了互斥访问,同一时刻只能有一个线程来对他操作。 - 可见性
多线程操作共享内存上的变量时,执行结果能够及时的同步到主内存,确保其他线程对此结果及时可见。 - 有序性
程序的执行顺序按照代码顺序执行,在单线程环境下,程序的执行都是有序的,但是在多线程环境下,JMM 为了性能优化,编译器和处理器会对指令进行重排,程序的执行会变成无序。
知道三个要素后,对上图而言,如果A、B两个线程同时操作x=0执行i++操作且都成功时,执行了两次i++,但最终结果i=1,那么就破坏了线程的原子性。
可见性就是加入A线程对x执行了+2的操作后直接同步到内存中,如果B线程在此后去写x,拿到的是x+2后的值。有序性是A线程先获得到x的值赋给y,再设置x=x+1,y的值为0,如果无序得话,可能先执行x=x+1,结果变为1。
2、synchronized
synchronized保证被修饰的方法或代码块操作的原子性、可见性和有序性。
- 被Synchronized 关键字描述的方法或代码块在多线程环境下同一时间只能由一个线程进行访问,因为在持有当前锁的线程执行完成之前,其他线程想要调用相关方法就必须进行排队,直到当前线程执行完成才释放锁给其他线程,所以保证了原子性。
- 被Synchronized 关键字描述的方法或代码块在多线程环境下数据是同步的,即当获取到锁后先将内存复制到自己的缓存中操作,释放锁之前会把缓存中的数据复制到共享内存中,所以保证了可见性。
- 被Synchronized 关键字描述的方法或代码块在多线程环境下同一时间只能由一个线程访问,代码内部是有序的,不会出发JMM指令重排机制,所以保证了有序性。
方法中被synchronized修饰锁的是当前的对象。synchronized锁不可中断,适合线程竞争不激烈。
synchronized可重入锁,可以在单例模式中使用,保证线程安全。
public class Singleton {
//双重校验锁
private volatile static Singleton singleton;
public Singleton() {
}
public static Singleton getInstance(){
if (singleton!=null){
synchronized (Singleton.class){
if (singleton!=null){
singleton=new Singleton();
}
}
}
return singleton;
}
}
3、volatile
volatile保证被修饰的变量的操作的可见性和有序性,但是不能保证原子性。适用与对变量的写操作不依赖当前值。或者该变量没有包含在具有其他变量的式子中,所以,volatile特别适合作为状态标记量。可以适用volatile+atomic来实现原子性。volatile只能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。但是volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
4、atomic
并发包下的AtomicBoolean、AtomicInteger、AtomicLong等类,使用这些类来声明变量时可以保证操作的原子性,内部实现了CAS操作保证原子性。
- ABA问题
在CAS操作时,其他线程将变量的值从A改成了B,然后又将B改回了A。 - 解决
每次变量改变时,将变量的版本号加1,只要变量被修改过,变量的版本号就会发生递增变化
5、ThreadLocal
线程局部变量,保证ThreadLocal包裹的对象在该线程里面不能被别的线程所修改。内部维护了一个ThreadLocalMap,简单理解为将当前线程作为key,值作为value存。一般用于多个线程都需要保存不同副本时。ThreadLocal和Synchronized都可以解决多线程中变量访问冲突,Synchronized是以时间换空间,ThreadLocal是以空间换时间。
6、Lock
并发包下的接口Lock,有ReentrantLock,ReentrantReadWriteLock.ReadLock有一系列的锁操作。可以灵活的加锁解锁,也可以打断锁,如果第一个线程占用锁时间太久,在等待的线程可以使用lockInterruptibly打断锁,同时抛出异常。在线程竞争激烈时,使用lock效率高。可以设置是否是公平锁。由于是用户自己手动加锁、解锁,所以可能会出现死锁清理,他有这些方法:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
可以通过tryLock(long time, TimeUnit unit)来实现尝试一段时间去获取锁,在这个时间内,拿不到就一直等待。
- ReentrantLock
可重入锁,即在两个方法都是同步的情况下,可以在一个方法中调用另一个,不用重新申请锁。 - ReentrantReadWriteLock
读锁和写锁,readLock()和writeLock()。当写方法加锁读方法不加锁时会出现脏读问题
6、生产者消费者问题
一般在实际业务中,wait都是配合while使用的,因为多个线程如果用if的话,可能导致notifyAll虚假唤醒,数据不一致。
问题1:
写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束。
1.两个程序互相使用wait和notify方法。
public class WhileFiveBreak {
//添加volatile,使t2能够得到通知
volatile List lists = new ArrayList();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
public static void main(String[] args) {
WhileFiveBreak c = new WhileFiveBreak();
final Object object = new Object();
new Thread(() -> {
synchronized(object) {
System.out.println("t2启动");
if(c.size() != 5) {
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 结束");
//通知t1继续执行
object.notify();
}
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
new Thread(() -> {
System.out.println("t1启动");
synchronized(object) {
for(int i=0; i<10; i++) {
c.add(new Object());
System.out.println("add " + i);
if(c.size() == 5) {
object.notify();
//释放锁,让t2得以执行
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1").start();
}
}
2.使用Latch(门闩)替代wait notify来进行通知,CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的
CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的。
public class WhileFiveBreak2 {
// 添加volatile,使t2能够得到通知
volatile List lists = new ArrayList();
public void add(Object o) {
lists.add(o);
}
public int size() {
return lists.size();
}
public static void main(String[] args) {
WhileFiveBreak2 c = new WhileFiveBreak2();
CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
System.out.println("t2启动");
if (c.size() != 5) {
try {
latch.await();
//也可以指定等待时间
//latch.await(5000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 结束");
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
new Thread(() -> {
System.out.println("t1启动");
for (int i = 0; i < 10; i++) {
c.add(new Object());
System.out.println("add " + i);
if (c.size() == 5) {
// 打开门闩,让t2得以执行
latch.countDown();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
}
}
问题2:
写一个固定容量同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程以及10个消费者线程的阻塞调用
1.使用wait和notifyall方法。
public class GetAndPut<T> {
final private LinkedList<T> lists = new LinkedList<>();
final private int MAX = 10; //最多10个元素
private int count = 0;
public synchronized void put(T t) {
while (lists.size() == MAX) { //想想为什么用while而不是用if?
try {
this.wait(); //effective java
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lists.add(t);
++count;
this.notifyAll(); //通知消费者线程进行消费
}
public synchronized T get() {
T t = null;
while (lists.size() == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
t = lists.removeFirst();
count--;
this.notifyAll(); //通知生产者进行生产
return t;
}
public static void main(String[] args) {
GetAndPut<String> c = new GetAndPut<String>();
//启动消费者线程
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) System.out.println(c.get());
}, "c" + i).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//启动生产者线程
for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 25; j++) c.put(Thread.currentThread().getName() + " " + j);
}, "p" + i).start();
}
}
}
2.使用condition指定生产者或消费者唤醒。
public class ConditionGetAndPut<T> {
final private LinkedList<T> lists = new LinkedList<>();
final private int MAX = 10; //最多10个元素
private int count = 0;
private Lock lock = new ReentrantLock();
private Condition producer = lock.newCondition();
private Condition consumer = lock.newCondition();
public void put(T t) {
try {
lock.lock();
while(lists.size() == MAX) { //想想为什么用while而不是用if?
producer.await();
}
lists.add(t);
++count;
consumer.signalAll(); //通知消费者线程进行消费
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public T get() {
T t = null;
try {
lock.lock();
while(lists.size() == 0) {
consumer.await();
}
t = lists.removeFirst();
count --;
producer.signalAll(); //通知生产者进行生产
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return t;
}
public static void main(String[] args) {
ConditionGetAndPut<String> c = new ConditionGetAndPut<String>();
//启动消费者线程
for(int i=0; i<10; i++) {
new Thread(()->{
for(int j=0; j<5; j++) System.out.println(c.get());
}, "c" + i).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//启动生产者线程
for(int i=0; i<2; i++) {
new Thread(()->{
for(int j=0; j<25; j++) c.put(Thread.currentThread().getName() + " " + j);
}, "p" + i).start();
}
}
}
问题3:
有N张火车票,每张票都有一个编号, 同时有10个窗口对外售票
1.使用阻塞队列:QueueLinkedList 卖票的时候poll是同步的
public class Question3 {
static Queue<String> tickets = new ConcurrentLinkedQueue<>();
static {
for(int i=0; i<1000; i++) tickets.add("票 编号:" + i);
}
public static void main(String[] args) {
for(int i=0; i<10; i++) {
new Thread(()->{
while(true) {
String s = tickets.poll();
if(s == null) break;
else System.out.println("销售了--" + s);
}
}).start();
}
}
}
问题4:
A线程正在执行一个对象中的同步方法,B线程是否可以同时执行同一个对象中的非同步方法?同步方法呢?
1.B线程可以同时访问非同步方法,不能同时访问同步方法,因为锁的是对象。如果同步方法出现异常,会释放锁。
public class Question4 {
public synchronized void s1(){
System.out.println("同步方法1开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("同步方法1结束");
}
public synchronized void s2(){
System.out.println("同步方法2开始");
System.out.println("同步方法2结束");
}
public void s3(){
System.out.println("非同步方法2");
}
public static void main(String[] args) {
Question1 c = new Question1();
new Thread(()->{
c.s1();
}).start();
new Thread(()->{
c.s2();
}).start();
new Thread(()->{
c.s3();
}).start();
}
}
问题5:
写一个程序模拟死锁
1.使用synchronized。
public class Question2 {
static Object o1 = new Object(), o2 = new Object();
public void s1() throws InterruptedException {
System.out.println("同步方法1开始");
synchronized (o1){
Thread.sleep(2000);
synchronized (o2){
System.out.println("获得o2");
}
}
System.out.println("同步方法1结束");
}
public void s2() throws InterruptedException {
System.out.println("同步方法2开始");
synchronized (o2){
Thread.sleep(2000);
synchronized (o1){
System.out.println("获得o1");
}
}
System.out.println("同步方法2结束");
}
public static void main(String[] args) {
Question2 c = new Question2();
new Thread(() -> {
try {
c.s1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
c.s2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
2.使用lock。
public class Question2 {
static Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();
public void s1() throws InterruptedException {
System.out.println("同步方法1开始");
lock1.lock();
Thread.sleep(1000);
lock2.lock();
lock1.unlock();
lock2.unlock();
System.out.println("同步方法1结束");
}
public void s2() throws InterruptedException {
System.out.println("同步方法2开始");
lock2.lock();
Thread.sleep(1000);
lock1.lock();
lock2.unlock();
lock1.unlock();
System.out.println("同步方法2结束");
}
public static void main(String[] args) {
Question2 c = new Question2();
new Thread(() -> {
try {
c.s1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
c.s2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}