一、认识线程
⼀个线程就是⼀个"执行流".每个线程之间都可以按照顺序执行自己的代码.多个线程之间"同时"执行着多份代码,(线程是轻量级进程)
线程存在的意义:
单核CPU的发展遇到了瓶颈.要想提⾼算⼒,就需要多核CPU.而并发编程能更充分利用多核CPU 资源
有些任务场景需要"等待IO",为了让等待IO的时间能够去做⼀些其他的工作,也需要用到并发编程
其次,虽然多进程也能实现并发编程,但是线程比进程更轻量.
创建线程比创建进程更快.
销毁线程比销毁进程更快.
调度线程比调度进程更快.
进程和线程的区别
1.进程是包含线程的,每个进程至少有一个线程,即主线程
2.进程与进程之间不共享内存空间,同一个进程的线程之间共享一个内存空间
3.进程是系统分配资源的最小单位,线程是系统调度的最小单位
4.一个进程挂了一般不影响其他进程,但是一个线程挂了可能会把同进程内的其他线程全部带走
二、多线程
线程创建
继承Thread类
java
package com.devilta.thread;
//继承Thread
class MyThread extends Thread {
//重写run方法
@Override
public void run() {
while (true) {
System.out.println("Hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demon1 {
public static void main(String[] args) throws InterruptedException {
//调用MyThread
Thread t = new MyThread();
t.start();
//main也调用
while (true) {
System.out.println("Hello mian");
Thread.sleep(1000);
}
}
}
或者
java
public class Demon1 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(){
@Override
public void run(){
while(true){
System.out.println("Hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
t.start();
while(true){
System.out.println("Hello main");
Thread.sleep(1000);
}
}
}
或者
java
public class Deon3 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true) {
System.out.println("Hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
while (true) {
System.out.println("Hello mian");
Thread.sleep(1000);
}
}
}
实现Runnable接口
java
package com.devilta.thread;
class MyRunnable implements Runnable{
@Override
public void run() {
while(true){
System.out.println("Hello Runnable");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demon2 {
public static void main(String[] args) throws InterruptedException {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
while(true){
System.out.println("Hello main");
Thread.sleep(1000);
}
}
}
或者
java
public class Demon2 {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("Hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
Thread t = new Thread(runnable);
t.start();
while(true){
System.out.println("Hello main");
Thread.sleep(1000);
}
}
}
Thread核心属性

如果线程不能阻止进程结束,那么这个线程就是后台线程,这里补充一个方法可以把线程设置成后台线程,setDaemon()
如果线程能左右进程的结束,或者说线程没结束,进程就不能结束,那么这个线程就是前台线程
终止一个线程
java
public class Demon4 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
while(!Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// throw new RuntimeException(e);
break;
}
}
System.out.println("t 结束");
});
t.start();
Thread.sleep(3000);
System.out.println("尝试终止线程t");
t.interrupt();
}
}
等待一个线程
让线程的执行顺序按照程序员的意愿来执行
java
public class Demon5 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for(int i = 0; i < 3; i++){
try {
System.out.println("hello thread");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t线程结束");
});
t.start();
t.join();
System.out.println("mian线程结束");
}
}
join也可以设置一个超时时间
java
public class Demon5 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for(int i = 0; i < 3; i++){
try {
System.out.println("hello thread");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t线程结束");
});
t.start();
t.join(3000);
System.out.println("mian线程结束");
}
}
三、线程状态
**• NEW:**安排了工作,还未开始行动
**• RUNNABLE:**可工作的.又可以分成正在工作中和即将开始工作
**• BLOCKED:**这几个都表示排队等着其他事情
**• WAITING:**这几个都表示排队等着其他事情
**• TIMED_WAITING:**这几个都表示排队等着其他事情
**• TERMINATED:**工作完成了.
主要理解每个状态的意义

四、线程安全
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的,否则是线程不安全的
举个例子:
java
public class Demon6 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for(int i = 0;i < 50000;i++){
count++;
}
System.out.println("t1结束");
});
Thread t2 = new Thread(() -> {
for(int i = 0;i < 50000;i++){
count++;
}
System.out.println("t2结束");
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
得到的结果并不是预期结果100000,出现这种情况的原因是count++
实际上**count++**分为三步
1.load:把内存中count的值,加载到cpu的寄存器
2.add:把寄存器中的值加一
3.save:把寄存器中的内容保存到内存上
但由于操作系统对线程的调度是随机的,抢占式执行的,因此执行上面三步骤时可能并不是一次性全部执行完毕,可能是执行到某个步骤就被调度走了
总结一下线程安全产生的原因:
1.根本原因:操作系统对线程的调度是随机的,即抢占式执行
2.多个线程修改同一个变量
3.修改操作不是原子的,所谓的原子类似于数据库事务的原子性,当修改操作只对应到一个CPU指令,就说明该操作是原子的,即不会出现指令执行到一半就被调度走了的情况,上面的例子就不是原子的操作
4.内存可见性
5.指令重排序
五、解决线程安全问题
先看代码:
java
public class Demon6 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Thread t1 = new Thread(() -> {
for(int i = 0;i < 50000;i++){
synchronized(lock){
count++;
}
}
System.out.println("t1结束");
});
Thread t2 = new Thread(() -> {
for(int i = 0;i < 50000;i++){
synchronized(lock){
count++;
}
}
System.out.println("t2结束");
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
这里的synchronized的作用相当于给count++加了个锁
synchronized的特性
互斥
synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行 到同⼀个对象synchronized就会阻塞等待.
• 进⼊synchronized修饰的代码块,相当于加锁
• 退出synchronized修饰的代码块,相当于解锁
synchronized的参数是构成互斥的关键,比如上面的代码中参数都是lock,这两个锁构成互斥,必须一个锁解锁后另一个锁才能进行加锁。如果这两个锁的参数不一样,那这样的锁就失去了意义
可重入
该特性主要解决了死锁问题
java
class Count{
public int count = 0;
public void add(){
//加锁
synchronized (this){
count++;
}
}
public int getCount(){
return count;
}
}
public class Demon1 {
public static void main(String[] args) throws InterruptedException {
Count count = new Count();
Thread t1 = new Thread(() ->{
for(int i = 0;i < 50000;i++){
//加锁
synchronized(count){
count.add();
}
}
});
t1.start();
t1.join();
System.out.println(count.getCount());
}
}
这段代码简单演示了一个死锁,首先在线程t1中,针对count.add()进行了加锁,那么其它地方想加锁就必须等待这个锁里的逻辑执行完毕后解锁,但是调用的这个方法本身又加了一层锁,这会导致代码会在这里一直阻塞等待下去,所以代码会卡在这里
但是因为java的synchronized具有可重入性,实际上代码是可以得到预期结果的
可重入锁的原理实际上是让内存把锁对象保存下来,即记录当前是哪个线程持有锁,当后续有线程针对这个锁再次加锁,判断是否是同一个线程,是的话就直接放行(实际上相当于没加锁),后续解锁的话,则通过一个计数器(初始为0)实现,每次匹配到 { 的话就加一,匹配到 } 就减一,当计数器再次为0时,就解锁
其他类型的死锁
可重入读解决的死锁是一个线程连续加两次同一把锁,死锁还有其他类型
两个线程,两把锁,线程本身已经加锁的情况下尝试获取对方的锁
先看代码:
java
import static java.lang.Thread.sleep;
public class Demon2 {
public static void main(String[] args) throws InterruptedException {
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(() ->{
synchronized (lock1) {
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock2) {
System.out.println("t1线程两个锁都拿到");
}
}
});
Thread t2 = new Thread(() ->{
synchronized (lock2) {
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock1) {
System.out.println("t1线程两个锁都拿到");
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
在上面的代码中每次获取对方锁前面都进行了等待,实际上这是为了成功演示死锁加的,如果不加的话可能不会死锁,由于执行速度的原因,可能t2还没开始,t1就已经拿到了两个锁
n个线程,m把锁
也就是哲学家就餐问题
-
有 5 个哲学家围坐在一张圆桌旁。
-
每个哲学家面前有一盘意大利面,桌子上的每个哲学家左右各有一根叉子 (有些版本是筷子)。
注:总共只有 5 根叉子。
-
每个哲学家的行为只有两种:思考 或 进食。
-
要进食,哲学家需要同时获得左右两边的叉子。
-
进食完毕后,他会放下两根叉子,继续思考。
如果每个哲学家都先拿起左边的叉子,然后等待右边的叉子,就会导致所有哲学家都拿着一根叉子,永远等不到第二根叉子 → 系统永久阻塞。
避免死锁
构成死锁的四个必要条件:
1.互斥,一个线程拿到锁后,另一个线程尝试获取锁,必须要阻塞等待
2.锁是不可抢占的
3.请求和保持,一个线程拿到锁1,不释放锁1的情况下获取锁2
4.循环等待,多个线程,多把锁之间等待对方释放锁
由于前两个必要条件是锁本身的特性,所以主要针对后两个条件进行处理
对于请求和保持,尽量不要嵌套请求锁,那么上面的示例可以这么改:
java
import static java.lang.Thread.sleep;
public class Demon2 {
public static void main(String[] args) throws InterruptedException {
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(() ->{
synchronized (lock1) {
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
synchronized (lock2) {
System.out.println("t1线程两个锁都拿到");
}
});
Thread t2 = new Thread(() ->{
synchronized (lock2) {
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
synchronized (lock1) {
System.out.println("t1线程两个锁都拿到");
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
这种做法打破了请求与保持,但有些场景下又必须进行嵌套加锁,因此需要针对循环等待进行处理
约定好加锁的顺序,比如先获取序号小的锁,再获取序号大的锁:
java
import static java.lang.Thread.sleep;
public class Demon2 {
public static void main(String[] args) throws InterruptedException {
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(() ->{
synchronized (lock1) {
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock2) {
System.out.println("t1线程两个锁都拿到");
}
}
});
Thread t2 = new Thread(() ->{
synchronized (lock1) {
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock2) {
System.out.println("t2线程两个锁都拿到");
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
内存可见性问题
先看代码:
java
import java.util.Scanner;
public class Demon3 {
private static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (flag == 0) {
}
System.out.println("t1结束");
});
Thread t2 = new Thread(() -> {
Scanner sc = new Scanner(System.in);
System.out.println("输入flag的值:");
flag = sc.nextInt();
});
t1.start();
t2.start();
}
}
两个线程,一个读取,一个写入,但是读的线程没有读到修改后的值,这就是内存可见性问题
为什么会产生这样的问题呢,因为编译器优化的原因,JVM会在程序员代码逻辑不变的基础上,对代码细节进行调整,是代码运行效率提高,但在多线程场景下,这种优化可能会导致代码逻辑前后出现偏差
在上面的示例里,有一个while循环,它的逻辑是flag为0的时候一直死循环,实际上是先把内存上的flag值load到寄存器里,然后执行类似于compare的指令,这两个指令的开销差距其实是比较大的,读内存的开销远远大于compare
在执行过程中,while循环的速度是相当快的,可能等到用户输入的时候已经执行了很多次循环,因此编译器会进行错误的优化,即将读取内存的操作变成读取寄存器,直接在寄存器上获取flag,所以当用户修改flag的值后,线程就感知不到了
怎么处理这种情况?
volatile关键字
java
import java.util.Scanner;
public class Demon3 {
private volatile static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (flag == 0) {
}
System.out.println("t1结束");
});
Thread t2 = new Thread(() -> {
Scanner sc = new Scanner(System.in);
System.out.println("输入flag的值:");
flag = sc.nextInt();
});
t1.start();
t2.start();
}
}
使用volatile可以有效解决内存可见性问题,但不能解决原子性问题
wait和notify
线程饥饿 (Thread Starvation)是指一个或多个线程无法获得所需的资源 (如 CPU 时间、锁、内存等),导致它们无法继续执行,而其他线程却一直在执行的现象。通俗的讲,由于操作系统的随即调度机制,当一个线程释放锁时,它还是就绪态, 其他线程都是阻塞等待,所以大概率还是这个线程拿到锁
这个场景就是wait和notify的典型场景
先看wait方法:
java
public class Demon4 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}
}
代码进入wait,就会先释放锁,然后阻塞等待,当其它线程做完了必要的工作,就会调用notify唤醒wait从而解除阻塞,重新获取到锁,继续执行
然后总体使用一下:
java
import java.util.Scanner;
public class Demon5 {
public static void main(String[] args) {
Object lock = new Object();
Thread t1 = new Thread(() ->{
try{
System.out.println("等待之前");
synchronized (lock){
lock.wait();
}
System.out.println("等待之后");
}catch (InterruptedException e){
throw new RuntimeException();
}
});
Thread t2 = new Thread(() ->{
Scanner sc = new Scanner(System.in);
System.out.println("输入内容以唤醒t1");
sc.nextInt();
synchronized (lock){
lock.notify();
}
});
t1.start();
t2.start();
}
}
这里注意几个点:
wait和notify的对象必须是同一个
一定要先有wait,才能notify
当有多个线程需要唤醒时,可以使用notifyAll
java
import java.util.Scanner;
public class Demon5 {
public static void main(String[] args) {
Object lock = new Object();
Thread t1 = new Thread(() ->{
try{
System.out.println("t1等待之前");
synchronized (lock){
lock.wait();
}
System.out.println("t1等待之后");
}catch (InterruptedException e){
throw new RuntimeException();
}
});
Thread t3 = new Thread(() ->{
try{
System.out.println("t3等待之前");
synchronized (lock){
lock.wait();
}
System.out.println("t3等待之后");
}catch (InterruptedException e){
throw new RuntimeException();
}
});
Thread t2 = new Thread(() ->{
Scanner sc = new Scanner(System.in);
System.out.println("输入内容以唤醒t1和t3");
sc.nextInt();
synchronized (lock){
lock.notifyAll();
}
});
t1.start();
t2.start();
t3.start();
}
}
当然wait也可以添加超时时间
wait和sleep的区别
1.wait必须要搭配锁使用,先加锁才能用,而sleep不需要
2.如果都在synchronized中使用,wait会释放锁,sleep不会
最后给个综合例子来看看wait和notify联动的效果:
java
public class Demon6 {
public static void main(String[] args) throws InterruptedException {
Object lock1 = new Object();
Object lock2 = new Object();
Object lock3 = new Object();
Thread t1 = new Thread(() ->{
try{
for(int i=0; i<10; i++){
synchronized (lock1){
lock1.wait();
}
System.out.print("A");
synchronized (lock2){
lock2.notify();
}
}
} catch (Exception e) {
throw new RuntimeException();
}
});
Thread t2 = new Thread(() ->{
try{
for(int i=0; i<10; i++){
synchronized (lock2){
lock2.wait();
}
System.out.print("B");
synchronized (lock3){
lock3.notify();
}
}
} catch (Exception e) {
throw new RuntimeException();
}
});
Thread t3 = new Thread(() ->{
try{
for(int i=0; i<10; i++){
synchronized (lock3){
lock3.wait();
}
System.out.println("C");
synchronized (lock1){
lock1.notify();
}
}
} catch (Exception e) {
throw new RuntimeException();
}
});
t1.start();
t2.start();
t3.start();
Thread.sleep(1000);
synchronized (lock1){
lock1.notify();
}
}
}