多线程 Java 线程和操作系统的线程有啥区别? JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,这是一种用户级线程(用户线程),也就是说 JVM 自己模拟了多线程的运行,而不依赖于操作系统。由于绿色线程和原生线程比起来在使用时有一些限制(比如绿色线程不能直接使用操作系统提供的功能如异步 I/O、只能在一个内核线程上运行无法利用多核),在 JDK 1.2 及以后,Java 线程改为基于原生线程(Native Threads)实现,也就是说 JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理。
我们上面提到了用户线程和内核线程,考虑到很多读者不太了解二者的区别,这里简单介绍一下:
用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。
内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。
顺便简单总结一下用户线程和内核线程的区别和特点:用户线程创建和切换成本低,但不可以利用多核。内核态线程,创建和切换成本高,可以利用多核。
一句话概括 Java 线程和操作系统线程的关系:现在的 Java 线程的本质其实就是操作系统的线程 。
线程模型是用户线程和内核线程之间的关联方式,常见的线程模型有这三种:
一对一(一个用户线程对应一个内核线程)
多对一(多个用户线程映射到一个内核线程)
多对多(多个用户线程映射到多个内核线程)
如何创建线程? 一般来说,创建线程有很多种方式,例如继承Thread
类、实现Runnable
接口、实现Callable
接口、使用线程池、使用CompletableFuture
类等等。
不过,这些方式其实并没有真正创建出线程。准确点来说,这些都属于是在 Java 代码中使用多线程的方法。
严格来说,Java 就只有一种方式可以创建线程,那就是通过new Thread().start()
创建。不管是哪种方式,最终还是依赖于new Thread().start()
创建线程的方法 继承Thread类 创建一个类继承Thread类
java.lang.Thread类: 是描述线程的类,我们想要实现多线程程序,就必须继承Thread类
实现步骤:
创建一个Thread类的子类
在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)
创建Thread类的子类对象
调用Thread类中的方法start方法 ,开启新的线程后执行run方法
void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。结果是两个线程并发地运行;当前线程(main线程)和另一个线程(创建的新线程,执行其 run 方法)。
多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
Java程序属于抢占式调度, 那个线程的优先级高,那个线程优先执行,同一个优先级,随机选择一个执行
1 2 3 4 5 6 7 8 9 10 11 public class MyThread extends Thread { @Override public void run () { for (int i = 0 ; i <20 ; i++) { System.out.println("run:" +i); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Demo01Thread { public static void main (String[] args) { MyThread mt = new MyThread (); mt.start(); for (int i = 0 ; i <20 ; i++) { System.out.println("main:" +i); } } }
构造方法:
public Thread(): 分配一个新的线程对象。
public Thread(String name) :分配一个指定名字的新的线程对象。
public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
public Thread(Runnable target,String name): 分配一个带有指定目标新的线程对象并指定名字
常用方法:
public String getName() : 获取当前线程名称。
public void start() : 导致此线程开始执行; Java虚拟机调用此线程的run方法。
public void run() : 此线程要执行的任务在此处定义代码。
public static Thread currentThread() :返回对当前正在执行的线程对象的引用。
public static void sleep(long millis): 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),毫秒数结束之后,线程继续执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class MyThread extends Thread { @Override public void run () { System.out.println(Thread.currentThread().getName()); } }
1 2 3 4 5 6 7 8 9 10 11 12 public static void main (String[] args) { MyThread mt = new MyThread (); mt.start(); new MyThread ().start(); new MyThread ().start(); System.out.println(Thread.currentThread().getName()); } }
main
Thread-0
Thread-1
Thread-2
为线程命名
void setName(String name) 改变线程名称,使之与参数name 相同。
public Thread(String name) 创建一个带参数的构造方法,参数传递线程的名称;调用父类的带参构造方法,把线程名称传递给父类,让父类(Thread)给子线程起一个名字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class MyThread extends Thread { public MyThread () {} public MyThread (String name) { super (name); } @Override public void run () { System.out.println(Thread.currentThread().getName()); } } public class Demo01SetThreadName { MyThread mt = new MyThread (); mt.setName("小强" ); mt.start(); new MyThread ("旺财" ).start(); new Thread (new Runnable () { @Override public void run () { System.out.println(Thread.currentThread().getName()); } }, "废材" ).start(); }
小强 废材 旺财
实现Runnable接口 Runnable接口应该由那些打算通过某一线程执行其实例的类来实现,Runnable接口的实现类类必须定义一个称为 run 的无参数方法
通过java.lang.Thread类的构造方法
Thread(Runnable target) 分配新的 Thread 对象。
Thread(Runnable target, String name) 分配新的 Thread 对象。
实现步骤:
创建一个Runnable接口的实现类
在实现类中重写Runnable接口的run方法,设置线程任务
创建一个Runnable接口的实现类对象
创建Thread类对象,构造方法中传递Runnable接口的实现类对象
调用Thread类中的start方法,开启新的线程执行run方法
实现Runnable接口创建多线程程序的好处:
避免了单继承的局限性
一个类只能继承一个类(一个人只能有一个亲爹),类继承了Thread类就不能继承其他的类
实现了Runnable接口,还可以继承其他的类,实现其他的接口
增强了程序的扩展性,降低了程序的耦合性(解耦)
实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)
实现类中,重写了run方法:用来设置线程任务
创建Thread类对象,调用start方法:用来开启新线程
1 2 3 4 5 6 7 8 9 10 11 public class RunnableImpl implements Runnable { @Override public void run () { for (int i = 0 ; i <20 ; i++) { System.out.println(Thread.currentThread().getName()+"-->" +i); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Demo01Runnable { public static void main (String[] args) { RunnableImpl run = new RunnableImpl (); Thread t = new Thread (run); t.start(); for (int i = 0 ; i <20 ; i++) { System.out.println(Thread.currentThread().getName()+"-->" +i); } } }
实现Callable接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;public class MyCallable implements Callable <Integer> { private final int num; public MyCallable (int num) { this .num = num; } @Override public Integer call () throws Exception { System.out.println("Starting task for number: " + num); Thread.sleep(2000 ); System.out.println("Task completed for number: " + num); return num * num; } public static void main (String[] args) { Callable<Integer> callable1 = new MyCallable (1 ); Callable<Integer> callable2 = new MyCallable (2 ); Callable<Integer> callable3 = new MyCallable (3 ); FutureTask<Integer> futureTask1 = new FutureTask <>(callable1); FutureTask<Integer> futureTask2 = new FutureTask <>(callable2); FutureTask<Integer> futureTask3 = new FutureTask <>(callable3); Thread thread1 = new Thread (futureTask1); Thread thread2 = new Thread (futureTask2); Thread thread3 = new Thread (futureTask3); thread1.start(); thread2.start(); thread3.start(); try { System.out.println("Result of task 1: " + futureTask1.get()); System.out.println("Result of task 2: " + futureTask2.get()); System.out.println("Result of task 3: " + futureTask3.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } }
创建了三个实现了 Callable
接口的 MyCallable
对象,并将它们分别传递给 FutureTask
的构造函数,得到三个 FutureTask
对象。然后,我们创建了三个线程,分别用这些 FutureTask
对象来初始化线程,并启动这些线程。
使用 FutureTask
可以直接将 Callable
对象传递给 Thread
构造函数,并且 FutureTask
本身实现了 Runnable
接口,因此可以作为线程的任务来执行。通过 get()
方法可以获取任务执行的结果,如果任务尚未完成,get()
方法会阻塞当前线程直到任务完成并返回结果。
可以直接调用 Thread 类的 run 方法吗? 这是另一个非常经典的 Java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
new 一个 Thread
,线程进入了新建状态。调用 start()
方法,会启动一个线程并使线程进入了就绪状态 ,当分配到时间片后就可以开始运行了。 start()
会执行线程的相应准备工作,然后自动执行 run()
方法的内容,这是真正的多线程工作。 但是,直接执行 run()
方法,会把 run()
方法当成一个 main 线程下的普通方法去执行 ,并不会在某个线程中执行它,所以这并不是多线程工作。
总结:调用 start()
方法方可启动线程并使线程进入就绪状态,直接执行 run()
方法的话不会以多线程的方式执行。
线程的生命周期和状态 Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
NEW: 初始状态,线程被创建出来但没有被调用 start()
。
RUNNABLE: 运行状态,线程被调用了 start()
等待运行的状态。
BLOCKED:阻塞状态,需要等待锁释放。
WAITING:等待状态,执行 wait()
方法之后,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
TIME_WAITING:超时等待状态,比如通过 sleep(long millis)
方法,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
TERMINATED:终止状态,表示该线程已经运行完毕。
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。
线程创建之后它将处于 NEW(新建) 状态,调用 start()
方法后开始运行,线程这时候处于 READY(可运行) 状态
可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。
当线程执行 wait()
方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。进入 WAITING 状态的线程不会消耗 CPU 资源,它会释放持有的锁(如果有的话)并进入等待队列中,直到被唤醒(等待其他线程同一个对象调用notify或者notifyAll方法)。
当线程进入 synchronized
方法/块或者调用 wait
后(被 notify
)重新进入 synchronized
方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。
TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)
方法或 wait(long millis)
方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。
线程在执行完了 run()
方法之后将会进入到 TERMINATED(终止) 状态。
wait、notify、notifyAll wait() 方法 :
当一个线程调用某个对象的 wait()
方法时,它会释放该对象的锁,并使线程进入等待状态(WAITING状态) ,直到其他线程调用相同对象的 notify()
或 notifyAll()
方法。
在调用 wait()
方法之前,线程必须拥有该对象的锁,通常通过 synchronized
块或方法来获得。
调用 wait()
方法后,线程会释放锁,使得其他线程可以进入同步块或同步方法。
线程在等待期间不会消耗CPU资源,因为它不会执行任何操作。
notify() 方法 :
当一个线程调用某个对象的 notify()
方法时,它会唤醒在该对象上等待的单个线程。
被唤醒的线程将从等待状态变为阻塞状态,并且再次尝试获取对象的锁。
如果对象的锁可用,线程将进入就绪状态,等待CPU调度执行。
如果没有线程在该对象上等待,notify()
方法不会做任何事情。
notifyAll() 方法 :
类似于 notify()
,但 notifyAll()
会唤醒在该对象上等待的所有线程。
所有被唤醒的线程将从等待状态变为阻塞状态,并尝试获取对象的锁。
通常,notifyAll()
用于确保所有等待的线程都有机会重新竞争锁。
注意:
调用wait和notify方法需要注意的细节wait方法与notify方法必须要由同一个锁对象调用 。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象 ,而任意对象的所属类都是继承了Object类的。
wait方法与notify方法必须要在同步代码块或者是同步函数中 使用。因为:必须要通过锁对象调用这2个方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class Main { public static void main (String[] args) { Object lock = new Object (); Thread producerThread = new Thread (() -> { synchronized (lock) { System.out.println("Producer thread is producing data." ); try { Thread.sleep(2000 ); lock.notify(); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread consumerThread = new Thread (() -> { synchronized (lock) { System.out.println("Consumer thread is waiting for data." ); try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Consumer thread is processing data." ); } }); producerThread.start(); consumerThread.start(); } }
2个线程轮流打印 创建2个线程轮流打印1-100
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class Main { private static final Object lock = new Object (); private static int count = 1 ; private static final int MAX_COUNT = 100 ; public static void main (String[] args) { Thread t1 = new Thread (() -> { while (count <= MAX_COUNT) { synchronized (lock) { if (count % 2 == 1 ) { System.out.println(Thread.currentThread().getName() + ": " + count++); lock.notify(); }else { try { lock.wait(); }catch (InterruptedException e){ e.printStackTrace(); } } } } }, "Thread-1" ); Thread t2 = new Thread (() -> { while (count <= MAX_COUNT) { synchronized (lock) { if (count % 2 == 0 ) { System.out.println(Thread.currentThread().getName() + ": " + count++); lock.notify(); }else { try { lock.wait(); }catch (InterruptedException e){ e.printStackTrace(); } } } } }, "Thread-2" ); t1.start(); t2.start(); } }
写两个线程,一个线程打印1~ 52,另一个线程打印A~Z,打印顺序是12A34B…5152Z
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class Main { private static final Object lock = new Object (); private static int number = 1 ; private static char letter = 'A' ; public static void main (String[] args) { Thread numberThread = new Thread (() -> { synchronized (lock) { for (int i = 1 ; i <= 26 ; i++) { System.out.print(number++); System.out.print(number++); lock.notify(); try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } }); Thread letterThread = new Thread (() -> { synchronized (lock) { for (int i = 1 ; i <= 26 ; i++) { System.out.print((char ) (letter++)); lock.notify(); try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } }); numberThread.start(); letterThread.start(); } }
3个线程轮流打印 3个线程轮流打印ABCABCABCABC…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public class Main { public static void main (String[] args) { Print print = new Print (); int count = 5 ; Thread t1 = new Thread (()->{ int i = 0 ; while (i < count){ i ++; print.myPrint( 0 ); } }); Thread t2 = new Thread (()->{ int i = 0 ; while (i < count){ i ++; print.myPrint(1 ); } }); Thread t3 = new Thread (()->{ int i = 0 ; while (i < count){ i ++; print.myPrint(2 ); } }); t1.start(); t2.start(); t3.start(); } public static class Print { private static int turn = 0 ; public synchronized void myPrint (int flag) { while (turn != flag){ try { this .wait(); } catch (InterruptedException e) { e.printStackTrace(); } } if (flag == 0 ){ System.out.println("A" ); }else if (flag == 1 ){ System.out.println("B" ); }else if (flag == 2 ){ System.out.println("C" ); } turn = (turn + 1 ) % 3 ; this .notifyAll(); } } }
编写一个程序,启动三个线程,三个线程的ID分别是1,2,3;每个线程依次打印1A,2B,3C,1D,2E,3F
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public class Main { public static void main (String[] args) { Print print = new Print (); Thread t1 = new Thread (()->{ while (true ){ print.myPrint( 0 ); } }); Thread t2 = new Thread (()->{ while (true ){ print.myPrint(1 ); } }); Thread t3 = new Thread (()->{ while (true ){ print.myPrint(2 ); } }); t1.start(); t2.start(); t3.start(); } public static class Print { private static int turn = 0 ; private static int count = 0 ; private static char zimu = 'A' ; public synchronized void myPrint (int flag) { while (turn != flag){ try { this .wait(); } catch (InterruptedException e) { e.printStackTrace(); } } zimu = (char )('A' + count); if (flag == 0 ){ System.out.println("1" + zimu); }else if (flag == 1 ){ System.out.println("2" + zimu); }else if (flag == 2 ){ System.out.println("3" + zimu); } count = (count + 1 ) % 26 ; turn = (turn + 1 ) % 3 ; this .notifyAll(); } } }
join方法 当一个线程调用另一个线程的 join()
方法时,它会被阻塞,直到被调用的线程执行完毕或者超时。在其他线程调用的线程执行完毕之前,调用 join()
方法的线程将一直保持阻塞状态。(如main线程调用其他线程的join,main会阻塞)
这个方法通常用于协调多个线程的执行顺序,常见的用法是在主线程中调用子线程的 join()
方法,以确保所有子线程都执行完毕后再继续执行主线程的后续逻辑。
创建10个线程,待他们执行完后主线程再执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class Main { public static void main (String[] args) { Thread[] threads = new Thread [10 ]; for (int i = 0 ; i < 10 ; i++) { threads[i] = new WorkerThread ("Thread " + i); threads[i].start(); } for (Thread thread : threads) { try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("All threads have finished execution. Main thread continues." ); } static class WorkerThread extends Thread { public WorkerThread (String name) { super (name); } @Override public void run () { System.out.println("Thread " + getName() + " is executing." ); try { Thread.sleep((long ) (Math.random() * 1000 )); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread " + getName() + " has finished execution." ); } } }
编写10个线程,第一个线程从1加到10,第二个线程从11加20…第十个线程从91加到100,最后再把10个线程结果相加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class Main { private static int count = 0 ; public static class SumThread extends Thread { int sum = 0 ; @Override public void run () { for (int i = 1 ; i <= 10 ; i++){ sum += count * 10 + i; } System.out.println(Thread.currentThread().getName() + " " + sum); } } public static void main (String[] args) { int res = 0 ; for (int i = 1 ; i <= 10 ; i++){ SumThread sumThread = new SumThread (); sumThread.setName("线程" +i); sumThread.start(); try { sumThread.join(); count ++; res += sumThread.sum; }catch (InterruptedException e){ e.printStackTrace(); } } System.out.println("所有线程的总和:" + res); } }
sleep() 方法和 wait() 方法区别 共同点 :两者都可以暂停线程的执行。
区别 :
sleep()
方法没有释放锁 ,但不占用CPU资源,而 wait()
方法释放了锁,也不占用CPU资源 。
wait()
通常被用于线程间交互/通信,sleep()
通常被用于暂停执行。
wait()
方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()
或者 notifyAll()
方法。sleep()
方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout)
超时后线程会自动苏醒。
sleep()
是 Thread
类的静态本地方法,wait()
则是 Object
类的本地方法。
为什么 wait() 方法不定义在 Thread 中? wait()
是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁 。每个对象(Object
)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object
)而非当前的线程(Thread
)。
类似的问题:为什么 sleep()
方法定义在 Thread
中?
因为 sleep()
是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
Java中线程的通信方式 线程之间的通信(inter-Thread Communication,ITC)主要依赖于共享内存。由于线程共享同一个进程的内存空间,因此可以直接通过共享变量进行通信。
常见的线程通信方式包括:
1)共享变量:
线程可以通过访问共享内存变量来交换信息(需要注意同步问题,防止数据竞争和不一致)共享的也可以是文件,例如写入同一个文件来进行通信。
2)同步机制:
Volatile:Java 中的关键字,确保变量的可见性,防止指令重排
synchronized:java 中的同步关键字,用于确保同一时刻只有一个线程可以访问共享资源,利用 Obiect 类提供的 wait() notify()、notifyA伦理、()实现线程之间的等待/通知机制
ReentrantLock:配合 Condition 提供了类似于 wait0)、notify( 的等待/通知机制
BlockingQueue:通过阻塞队列实现生产者-消费者模式
CountDownLatch:可以允许一个或多个线程等待,直到在其他线程中执行的一组操作完成
CyclicBarrier:可以让一组线程互相等待,直到到达某个公共屏障点
Semaphore:信号量,可以控制对特定资源的访问线程数
Java线程的死锁 什么是线程死锁?如何避免死锁? 线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class DeadLockDemo { private static Object resource1 = new Object (); private static Object resource2 = new Object (); public static void main (String[] args) { new Thread (() -> { synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1" ); try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource2" ); synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2" ); } } }, "线程 1" ).start(); new Thread (() -> { synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2" ); try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource1" ); synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1" ); } } }, "线程 2" ).start(); } }
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1
线程 A 通过 synchronized (resource1)
获得 resource1
的监视器锁,然后通过Thread.sleep(1000);
让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。
上面的例子符合产生死锁的四个必要条件:
互斥条件:该资源任意一个时刻只由一个线程占用。
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
如何预防和避免线程死锁? 如何预防死锁? 破坏死锁的产生的必要条件即可:
破坏请求与保持条件 :一次性申请所有的资源。
破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
如何避免死锁?
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3……Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称 <P1、P2、P3.....Pn>
序列为安全序列。
如何排查线程死锁? 首先使用jps查找程序的线程
然后使用jstack 线程id,就可以找到死锁线程
如下图,t2正在等待锁对象的地址为0x000000076b4alaf8,而t1正在锁住该对象,所以发生了思死锁
线程在内存中如何保证安全性? 在堆内存,所有的线程都可以访问到该堆区域,因此如果有一个共享的变量可以被所有的线程访问到,就有可能产生安全风险。
解决方法
私有化: 比如局部变量,每个线程都有自己的栈空间,可以保证只能操作自己的数据
不共享: 保证公共区域的数据安全,ThreadLocal线程本地存储是一种特殊的数据存储区域,每个线程都有自己的线程本地存储区域,不同线程之间互不干扰,从而避免了线程间数据共享的安全问题。
只读: final关键字,如常量, 但是final
修饰的对象的引用地址不可变,但对象的状态(即对象内部的成员变量值)仍然可以被修改。如果对象状态是可变的,并且被多个线程同时访问和修改,那么仍然可能出现线程安全问题。
加锁: Java 提供了多种同步机制,如 synchronized 关键字、Lock 接口、Atomic 类等,用于控制对共享资源的访问,防止多个线程同时访问导致的数据竞争和安全问题。同步机制可以保证在同一时刻只有一个线程访问共享资源,从而确保了线程安全性。
CAS: CAS 操作是一种无锁的并发控制机制,CAS 主要适用于对单个共享变量进行原子操作的场景,例如计数器、标记位等。CAS 是一种乐观锁机制,它假设并发冲突不会经常发生,因此在高并发、高竞争的情况下,CAS 可能会导致自旋等待,性能下降。