文章目录
一、进程和线程
- 进程
- 进程代表了内存中正在运行的应用程序,计算机中的资源(cpu 内存 磁盘 网络等),会按照需求分配给每个进程,从而这个进程对应的应用程序就可以使用这些资源了。
- 进程就是在系统中,运行一个应用程序的基本单位。
- 线程
- 线程是进程中的一个代码执行单元,负责当前进程中代码程序的执行
- 一个进程中有一个或多个线程。
二、并发和并行
- 线程的并发执行,是指在一个时间段内,俩个或多个线程,使用一个CPU,进行交替运行。
- 线程的并行执行,是指在同一时刻,俩个或多个线程,各自使用一个CPU,同时进行运行。
- 如果计算机是单核CPU的话,那么同一时刻只能有一个线程使用CPU来执行代码
- 如果计算机是多核CPU的话,那么同一时刻有可能是俩个线程同时使用不同的CPU执行代码
三、时间片
- 时间片
- 时间片,当前一个线程要使用CPU的时候,CPU会分配给这个线程一小段时间(毫秒级别),这段时间就叫做时间片。
————也就是该线程允许使用CPU运行的时间,在这个期间,线程拥有CPU的使用权。 - 如果在一个时间片结束时,线程还在运行,那么这时候,该线程就需要停止运行,并交出CPU的使用权,然后等待下一个CPU时间片的分配。
- 俩个线程在使用一个CPU的时候,它们是交替着运行的,每个线程每次都是运行一个很小的时间片,然后就交出CPU使用权
- 调度方式
- 时间片轮转
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。 - 抢占式调度
JVM中的线程,使用的为抢占式调度。
系统会让优先级高的线程优先使用 CPU(提高抢占到的概率),但是如果线程的优先级相同,那么会随机选择一个线程获取当前CPU的时间片。
public static void main(String[] args) {
//创建线程对象t1
Thread t1 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("hello");
}
}
};
//创建线程对象t2
Thread t2 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("world");
}
}
};
//启动线程t1 t2
t1.start();
t2.start();
}
四、main线程
Thread.currentThread();
可以写在任意方法中,返回就是执行这个方法的线程对象
public class Test {
public static void main(String[] args) {
//获取执行当前方法的线程对象
Thread currentThread = Thread.currentThread();
System.out.println("执行当前方法的线程名字为:"+currentThread.getName());
}
}
//运行结果:
执行当前方法的线程名字为:main
五、线程的创建和启动
java.lang.Thread
是java中的线程类,所有的线程对象都必须是Thread
类或其子类的实例。
每个线程的作用,就是完成我们给它指定的任务,实际上就是执行一段我们指定的代码。我们只需要在Thread
类的子类中重写 run
方法,把执行的代码写入到run
方法中即可,这就是线程的执行任务!
Java中通过继承Thread类来创建并启动一个新的线程的步骤如下:
- 定义
Thread
类的子类(可以是匿名内部类),并重写Thread
类中的run
方法,run
方法中的代码就是线程的执行任务- 创建
Thread
子类的对象,这个对象就代表了一个要独立运行的新线程- 调用线程对象的
start
方法来启动该线程
(1)定义 Thread 类的子类
public class Test {
public static void main(String[] args) {
//2.创建线程类对象
Thread t = new MyThread();
//3.调用start方法启动线程
t.start();
}
}
//1.子类继承父类Thread,并重写run方法(指定线程的执行任务)
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("hello world");
try {
//可以让当前执行代码的线程睡眠1000毫秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
(2)匿名内部类
public class Test {
public static void main(String[] args) {
Thread t = new MyThread(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("hello world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t.start();
}
}
六、Runnable接口
- 给一个线程对象指定要执行的任务,除了继承
Thread
类后重写run
方法之外,还可以利于Runnable接口来完成线程任务的指定 java.lang.Runnable
,该接口中只有一个抽象方法run
Thread
类也是Runnable
接口的实现类- 子类重写
Thread
中的run
方法,这个run
方法其实也来自于Runnable
接口 - 实现
Runnable
接口比继承Thread
类所具有的优势:- 可以把相同的一个执行任务(
Runnable
接口的实现),交给不同的线程对象去执行 - 可以避免java中的单继承的局限性。
- 线程和执行代码各自独立,实现代码解耦
- 可以把相同的一个执行任务(
使用 Runnable
接口的匿名内部类,来指定线程的执行任务(重写接口中的run
方法):
public class Test {
public static void main(String[] args) {
//Runnable接口的实现类中,重写了run方法,指定线程的执行任务
Runnable run = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("hello world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//创建线程对象,指定执行任务
Thread t = new Thread(run);
t.start();
}
}
七、线程的名字
- 获取当前线程的名字
- 通过
Thread
类中的currentThread
方法,可以获取当前线程的对象,然后调用线程对象的getName
方法,可以获取当前线程的名字。String name = Thread.currentThread().getName();
start
方法启动线程后,线程会自动执行run
方法- 千万不要直接调用
run
方法,这样就不是启动线程执行任务,而是普通的方法调用
public class Test {
public static void main(String[] args) {
String name = Thread.currentThread().getName();
System.out.println("执行当前main方法的线程是:"+name);
Runnable run = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println("执行当前run方法的线程是:"+name);
}
};
Thread t = new Thread(run);
t.start();
}
}
//运行结果为:
执行当前main方法的线程是:main
执行当前run方法的线程是:Thread-0
- 线程的默认名字
- 默认情况下,主线程中,创建出的线程,它们的都会有一个默认的名字
"Thread-" + nextThreadNum()
就是在拼接出这个线程默认的名字,Thread-0 Thread-1 Thread-2等等
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
- 指定线程的名字
可以创建线程对象的时候,给它设置一个指定的名字:
Thread t = new Thread("t线程");
//或者
Thread t = new Thread(new Runnable(){
public void run(){ //执行任务 }
},"t线程");
//或者
Thread t = new Thread();
t.setName("t线程");
八、线程的分类
- 前台线程,又叫做执行线程、用户线程
- 这种线程专门用来执行用户编写的代码,地位比较高,JVM是否会停止运行,就是要看当前是否还有前台线程没有执行完,如果还剩下任意一个前台线程没有“死亡”,那么JVM就不能停止!
- 执行程序入口的主线程(main),就是一个前台线程
- 在主线程创建并启动的新线程,默认情况下就是一个前台线程,用来执行用户编写的代码任务。
- 后台线程,又叫做守护线程、精灵线程
- 这种线程是用来给前台线程服务的,给前台线程提供一个良好的运行环境,地位比较低,JVM是否停止运行,根本不关心后台线程的运行情况和状态。
在主线程中,创建出来的线程对象,默认就是前台线程,在它启动之前,我们还可以给它设置为后台线程:
public class Test {
public static void main(String[] args) {
Thread t = new Thread("t线程"){
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 10; i++) {
System.out.println(name+": hello "+i);
}
}
};
//在启动线程之前,可以将其设置为后台线程,否则默认是前台线程
t.setDaemon(true);
t.start();
}
}
九、线程优先级
- 最终设置线程优先级的方法,是一个native方法,并不是java语言实现的
- 线程的优先级使用int类型数字表示,最大是10,最小是1,默认的优先级是5
- 当俩个线程争夺CPU时间片的时候:
- 优先级相同,获得CPU使用权的概率相同
- 优先级不同,那么高优先级的线程有更高的概率获取到CPU的使用权
十、线程组
- Java中使用
java.lang.ThreadGroup
类来表示线程组,它可以对一批线程进行管理,对线程组进行操作,同时也会对线程组里面的这一批线程操作。 - 创建线程组的时候,需要指定该线程组的名字。也可以指定其父线程组,如果没有指定,那么这个新创建的线程组的父线程组就是当前线程组。
(1)主线程中,创建一个线程对象,它的线程组默认就是当前线程的线程组
public class Test {
public static void main(String[] args) {
Thread t = new Thread();
ThreadGroup threadGroup = t.getThreadGroup();
System.out.println(threadGroup);
}
}
//运行结果:
java.lang.ThreadGroup[name=main,maxpri=10]
(2)只有在创建线程对象的时候,才能指定其所在的线程组,线程运行中途不能改变它所属的线程组
public class Test {
public static void main(String[] args) {
ThreadGroup group = new ThreadGroup("我的线程组");
Runnable run = new Runnable() {
@Override
public void run() {
try {
//让线程休眠一会,否则运行太快,死亡太快了
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t1 = new Thread(group,run,"t1线程");
Thread t2 = new Thread(group,run,"t2线程");
Thread t3 = new Thread(group,run,"t3线程");
//注意,启动后,三个线程都会进行休眠,等run方法运行完就“死亡”了
t1.start();
t2.start();
t3.start();
//返回当前线程组中还没有“死亡”的线程个数
System.out.println("线程组中还在存活的线程个数为:"+group.activeCount());
//准备好数组,保存线程组中还存活的线程
Thread[] arr = new Thread[group.activeCount()];
//将存活的线程集中存放到指定数组中,并返回本次存放到数组的存活线程个数
System.out.println("arr数组中存放的线程个数为:"+group.enumerate(arr));
//输出数组中的内容
System.out.println("arr数组中的内容为:"+Arrays.toString(arr));
}
}
//运行结果:
线程组中还在存活的线程个数为:3
arr数组中存放的线程个数为:3
arr数组中的内容为:[Thread[t1线程,5,我的线程组], Thread[t2线程,5,我的线程组], Thread[t3线 程,5,我的线程组]]
十一、线程状态
-
java.lang.Thread.State
枚举类型中(内部类形式),定义了线程的几种状态 -
BLOCKED,WAITING,TIMED_WAITING 这三种都属于线程阻塞,只是触发的条件不同,以及从阻塞状态中恢复过来的条件也不同而已。
-
线程在这三种情况的阻塞下,都具备相同的特点:
- 线程不执行代码
- 线程也不参与CPU时间片的争夺
-
状态描述和解释如下:
线程状态 | 名称 | 描述 |
---|---|---|
NEW | 新建 | 线程刚被创建,还没调用start方法,或者刚刚调用了start方法,调用start方法不一定"立即"改变线程状态,中间可能需要一些步骤才完成一个线程的启动。 |
RUNNABLE | 可运行 | start方法调用结束,线程由NEW变成RUNNABLE,线程存活着,并尝试抢占CPU资源,或者已经抢占到CPU资源正在运行,这俩种情况的状态都显示为RUNNABLE |
BLOCKED | 锁阻塞 | 线程A和线程B都要执行方法test,而且方法test被加了锁,线程A先拿到了锁去执行test方法,线程B这时候需要等待线程A把锁释放。这时候线程B就是处理BLOCKED |
WAITING | 无限期等待 | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
TIMED_WAITING | 有限期等待 | 和WAITING状态类似,但是有一个时间期限,时间到了,自己也会主动醒来 |
TERMINATED | 终止(死亡) | run方法执行结束的线程处于这种状态。 |
- 一个线程,经历的最普通的过程如下:
- 刚创建好的线程对象,就是出于
NEW
的状态 - 线程启动后,会出于
RUNNABLE
状态 - 其实这个
RUNNABLE
状态包含俩种情况:- 就绪状态,此时这个线程没有运行,因为没有抢到CPU的执行权
- 运行状态,此时这个线程正在运行中,因为抢到CPU的执行权
- JavaAPI中并没有定义就绪状态和运行状态,而是把这俩情况统一叫做
RUNNABLE
(可运行状态),但是一般我们为了能更加清楚的描述问题,会用上就绪状态和运行状态 - 在线程多次抢到CPU执行权,“断断续续”把run方法执行完之后,就变成了
TERMINATED
状态(死亡),之所以是“断断续续”的运行,是因为每次抢到CPU执行权的时候,只是运行很小的一个时间片,完了之后还要重新抢夺下一个时间片,并且中间还有可能抢不到的情况 - 死亡后的线程,不能重新启动
- 刚创建好的线程对象,就是出于
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread("t1线程"){
@Override
public void run() {
for (int i = 0; i < 10; i++) { }
}
};
System.out.println(t1.getState());
//启动t1线程
t1.start();
System.out.println(t1.getState());
System.out.println(t1.getState());
System.out.println(t1.getState());
System.out.println(t1.getState());
System.out.println(t1.getState());
System.out.println(t1.getState());
System.out.println(t1.getState());
System.out.println(t1.getState());
}
}
//运行结果:
注意需要多运行几次,因为可能每次运行的情况不一样
NEW
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
TERMINATED
TERMINATED
十二、线程类Thread中的方法
1.sleep方法
- 该静态方法可以让当前执行的线程暂时休眠指定的毫秒数
- 线程执行了
sleep
方法后,会从RUNNABLE
状态进入到TIMED_WAITING
状态 - 线程所处的是一种阻塞状态,这种阻塞的特点是:阻塞结束后,线程会自动回到
RUNNABLE
状态
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread("t1线程"){
@Override
public void run() {
try {
//t1线程休眠10毫秒
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//t1还没有启动,这里肯定是NEW状态
System.out.println(t1.getState());
//启动t1线程
t1.start();
//在循环期间查看t1的状态1000次
//这里t1的状态可能是RUNNABLE,也可能是TIMED_WAITING,也可能是TERMINATED
for (int i = 0; i < 1000; i++) {
System.out.println(t1.getState());
}
}
}
//运行结果:
可以多运行几次,每次结果可能不一样
NEW
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
.....
RUNNABLE
RUNNABLE
TERMINATED
TERMINATED
TERMINATED
TERMINATED
.....
2. join方法
使用join
方法,可以让当前线程阻塞,等待另一个指定的线程运行结束后,当前线程才可以继续运行
(1)使用无参的join
方法
- 线程执行了
join()
方法后,会从RUNNABLE
状态进入到WAITING
状态 - t2线程中,调用了t1对象的
join
方法,那么t2线程就会阻塞,等待t1线程的运行结束,t2线程才能恢复
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread("t1线程"){
@Override
public void run() {
try {
//t1线程睡眠1秒钟
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1线程结束");
}
};
Thread t2 = new Thread("t2线程"){
@Override
public void run() {
try {
//t2线程调用t1.join方法
//t2线程进入阻塞状态
//t2线程要等到t1线程运行结束,才能恢复到RUNNABLE状态
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2线程结束");
}
};
t1.start();
t2.start();
//让主线程休眠500毫秒,目的是为了给t1和t2点时间,让他们俩个线程进入状态
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t2.getState());
}
}
//运行结果:
WAITING
t1线程结束
t2线程结束
(2)使用有参的join
方法
- 线程执行了
join(long million)
方法后,会从RUNNABLE
状态进入到TIMED_WAITING
状态
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread("t1线程"){
@Override
public void run() {
try {
//t1线程睡眠1秒钟
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1线程结束");
}
};
Thread t2 = new Thread("t2线程"){
@Override
public void run() {
try {
//t2线程调用t1.join方法
//t2线程进入阻塞状态
//t2线程要等到t1线程运行结束,才能恢复到RUNNABLE状态
//2000表示,当前线程t2最多阻塞2秒钟,2秒钟之内t1线程没有结束,那么t2线 程就自动恢复
t1.join(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2线程结束");
}
};
t1.start();
t2.start();
//让主线程休眠500毫秒,目的是为了给t1和t2点时间,让他们俩个线程进入状态
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t2.getState());
}
}
//运行结果:
TIMED_WAITING
t1线程结束
t2线程结束
- 如果指定了时间,线程阻塞一定的时间后,会自动恢复到
RUNNABLE
状态,这种情况下,线程的状态为TIMED_WAITING
(有限期等待)- 如果没有指定时间,线程会一直阻塞着,直到某个条件满足时,才会自动恢复,这种情况下,线程的状态为
WAITING
(无限期等待)
3.interrupt方法
sleep
方法和join
方法可知,这俩个方法都会抛出InterruptedException
类型的异常InterruptedException
异常类型指的是:线程A中,调用了线程B的interrupt
方法,而此时线程B处于阻塞状态,那么此时sleep
方法或者join
方法就会抛出被打断的异常
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread("t1线程"){
@Override
public void run() {
try {
//t1线程休眠100秒
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1线程结束");
}
};
t1.start();
//让主线程休眠500毫秒,目的是为了给t1时间,让它调用sleep方法而进入阻塞状态
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打断t1由于调用sleep方法而进入的阻塞状态
t1.interrupt();
}
}
//运行结果:
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.briup.sync.Test$1.run(Test.java:11)
t1线程结束
interrupt
方法的工作原理:
interrupt
方法是通过改变线程对象中的一个标识的值(true|false),来达到打断阻塞状态的效果。一个线程在阻塞状态下,会时刻监测这个标识的值是不是true,如果一旦发现这个值变为true,那么就抛出异常结束阻塞状态,并再把这个值改为false。- 查看线程对象中“打断标识”值的俩个方法:
(1)非静态方法
只是返回这个“打断标识”值,并且不会对这个值进行清除(true- >false),因为所传参数ClearInterrupted的值为false
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread("t1线程"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//判断是否有其他线程调用了自己的interrupt方法
//调用类中的非静态方法:isInterrupted
System.out.println(this.isInterrupted());
}
System.out.println("t1线程结束");
}
};
t1.start();
}
}//运行结果:
false
false
false
false
false
false
false
false
false
false
t1线程结束
(2)静态方法
返回这个“打断标识”值,并且会对这个值进行清除(true->false),因为所传参数ClearInterrupted的值为true
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread("t1线程"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//判断是否有其他线程调用了自己的interrupt方法
//调用类中的静态方法:interrupted
System.out.println(Thread.interrupted());
}
System.out.println("t1线程结束");
}
};
t1.start();
t1.interrupt();
}
}
//运行结果:
true
false
false
false
false
false
false
false
false
false
t1线程结束
十三、线程安全
JVM内存中的堆区,是一个共享的区域,是所有线程都可以访问的内存空间。
JVM内存中的栈区,是线程的私有空间,每个线程都有自己的栈区,别的线程无法访问到自己栈区的数据。
如果有多个线程,它们在一段时间内,并发访问堆区中的同一个变量,并且有写入的操作,那么最终可能会出数据的结果和预期不符的情况,这种情况就是线程安全问题。
- t1和t2并发访问的时候,争夺CPU的时间片,运行完时间片,退出后再次争夺下一个时间片,也就是说t1和t2都是“断断续续”的运行的
- 在这期间,可能t1线程有一次拿到时间片运行的时候,给num赋值为1,然后时间片用完退出了,结果下次t2线程拿到了时间片,又将num的值赋成了11750,然后t1线程又拿到了时间片,本来预期的是输出1,但是结果却是输出了11750
- 核心的原因是,t1线程操作一下变量num,然后时间片用完退出去,t2先过来又操作了变量num,等t1线程再过来的时候,这值已经被t2线程给“偷偷”修改了,那么就出现了和预期不符的情况
public class Test {
public static void main(String[] args) {
MyData myData = new MyData();
Thread t1 = new Thread("t1"){
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 10; i++) {
//先给num赋值 myData.num = i;
//然后再输出num的值
System.out.println(name + ": " + myData.num);
}
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() {
for (int i = 100; i < 20000; i++) {
//给num赋值 myData.num = i;
}
}
};
t1.start();
t2.start();
}
}
class MyData{
int num;
}
//运行结果:
t1: 0
t1: 11706
t1: 13766
t1: 15459
t1: 17710
t1: 19304
t1: 6
t1: 7
t1: 8
t1: 9
十四、 线程同步(解决线程安全问题)
- Java中实现线程同步的方式,是给需要同步的代码进行
synchronized
关键字加锁。 - 线程同步的效果,就是一段加锁的代码,每次只能有一个拿到锁的线程,才有资格去执行,没有拿到的锁的线程,只能等拿到锁的线程把代码执行完,再把锁给释放了,它才能去拿这个锁然后再运行代码。
- 本来这段代码是俩线程并发访问,“争先恐后”的去执行的,现在线程同步之后,这段代码就变成了先由一个拿到锁的线程去执行,执行完了,再由另一个线程拿到锁去执行。
public class Test {
public static void main(String[] args) {
MyData myData = new MyData();
Thread t1 = new Thread("t1"){
@Override
public void run() {
String name = Thread.currentThread().getName();
synchronized (myData){
for (int i = 0; i < 10; i++) {
myData.num = i;
System.out.println(name + ": " + myData.num);
}
}
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() {
synchronized (myData){
for (int i = 100; i < 20000; i++) {
myData.num = i;
}
}
}
};
t1.start();
t2.start();
}
}
class MyData{
int num;
}
- 这时候t2线程的阻塞状态属于锁阻塞,需要等待另一个线程把锁释放了,t2线程才能恢复。如果t2线程处于这种阻塞,那么调用线程对象的 getState 方法返回的状态名称为:
BLOCKED
- t1线程需要拿到锁对象obj,才能运行加锁的代码块
- t2线程也需要拿到锁对象obj,才能运行加锁的代码块
- 锁对象obj只有一个,所以t1和t2只能有一个线程先拿到,拿到后执行代码,那么另一个就拿不到了,拿不到就阻塞,此时线程的状态为:
BLOCKED
- 任意一个对象,只要是对象,就可以用来当做,加锁代码块中的锁对象。然后让多个线程,去争抢这把锁就可以了,此时就达到了线程同步的效果,因为只有拿到锁的线程,能执行代码,其 他拿不到的线程就不执行,并且进入阻塞状态。
public class Test {
public static void main(String[] args) {
Object obj = new Object();
Thread t1 = new Thread("t1"){
@Override
public void run() {
synchronized (obj){
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() {
synchronized (obj){ }
}
};
t1.start();
//主线程休眠1秒钟,给t1线程点时间,让他先拿到锁,然后去休眠100秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
System.out.println("t1线程状态:"+t1.getState());
System.out.println("t2线程状态:"+t2.getState());
}
}
//运行结果:
t1线程状态:TIMED_WAITING
t2线程状态:BLOCKED
十五、synchronized
synchronized
修饰一个代码块,并指定谁是锁对象的用法,除此之外,还可以使用 synchronized 直接修饰一个方法,表示这个方法中的所有代码都需要线程同步。synchronized
关键字修饰非静态方法,默认使用this
当做锁对象,并且不能自己另外指定synchronized
关键字修饰静态方法,默认使用当前类的Class对象
当做锁对象,并且不能自己另外指定
这俩中情况的同步效果是一样的,只是锁对象不同而已- 可以直接在
add
方法(非静态方法)上,添加修饰符synchronized
关键字,表示给这个方法中的所有代码进行线程同步,默认使用的锁对象是this
该代码表示,拿到锁对象
this
的线程,才可以进入到add
方法中执行代码,代码执行完,会释放锁,这时锁变的可用了,所有需要这把锁的线程都恢复到RUNABLE状态(它们之前在锁阻塞状态),这些线程一起重新争夺CPU执行权,谁先拿到CPU执行权,就会先过去拿到锁,进入代码去执行
线程同步的效果的关键点在于,让t1和t2俩个线程去争夺同一把锁对象
public synchronized void add(int num){
String name = Thread.currentThread().getName();
arr[current] = num;
System.out.println(name+"线程本次写入的值为"+num+",写入后取出的值为"+arr[current]);
current++;
}
public class Test {
public static void main(String[] args) {
MyData myData = new MyData();
Thread t1 = new Thread("t1"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
myData.add(i);
//计算机运行10次运行太快了,让它执行慢一些,好观察效果
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//t2线程的名字前面加个制表符\t,打印的时候好观察
Thread t2 = new Thread("\tt2"){
@Override
public void run() {
for (int i = 10; i < 20; i++) {
myData.add(i);
//计算机运行10次运行太快了,让它执行慢一些,好观察效果
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
t2.start();
}
}
十六、锁对象的三个方法
方法:
wait()
、notify()
、notifyAll
- 三个核心点:
- 任何对象中都一定有这三个方法
- 只有对象作为锁对象的时候,才可以调用
- 只有在同步的代码块中,才可以调用
(1)wait
方法:可以让拿到的锁的线程,即使代码没执行完,也可以把锁立即给释放了
- 线程调用了
wait
方法,释放了锁,变为阻塞状态(WAITING
),并进入了等待池,等待其他线程唤醒自己或者打断自己,如果有线程调用了notify
方法进行了唤醒,或者interrupt
方法进行了打断,那么这个线程就会从等待池进入到锁池,而进入到锁池的线程,会时刻关注锁对象是否可用,一旦可用,这个线程就会立刻自动恢复到RUNNABLE
状态。
TIMED_WAITING
、WAITING
、BLOCKED
都属于线程阻塞,他们共同的特点是就是线程不执行代码,也不参与CPU的争夺,除此之外,它们还有各自的特点:(重要)
- 线程运行时,调用
sleep
或者join
方法后,进入这种阻塞,该阻塞状态可以恢复到RUNNABLE
状态,条件是线程被打断了、或者指定的时间到了,或者join
的线程结束了- 线程运行时,发现锁不可用后,进入这种阻塞,该阻塞状态可以恢复到
RUNNABLE
状态,条件是线程需要争夺的锁对象变为可用了(别的线程把锁释放了)- 线程运行时,调用了
wait
方法后,线程先释放锁后,再进入这种阻塞,该阻塞状态可以恢复到BLOCKED
状态(也就是阻塞2的情况),条件是线程被打断了、或者是被别的线程唤醒了(notify
方法)
- 希望的是t1线程中i=5的时候,先释放锁,让t2拿到锁去运行,在t2线程中,当j=15的时候,释放锁,让t1拿到锁去运行:
public class Test {
public static void main(String[] args) {
final Object obj = new Object();
Thread t1 = new Thread("t1"){
@Override
public void run() {
String name = Thread.currentThread().getName();
synchronized (obj){
for (int i = 0; i < 10; i++) {
System.out.println(name+"线程: i = "+i);
if(i==5){
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() {
String name = Thread.currentThread().getName();
synchronized (obj){
for (int j = 10; j < 20; j++) {
System.out.println(name+"线程: j = "+j);
if(j==15){
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
};
t1.start();
t2.start();
//主线程休眠1秒钟,给t1和t2点时间,等它们调用wait方法
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1线程当前的状态为:"+t1.getState());
System.out.println("t2线程当前的状态为:"+t2.getState());
}
}
//运行结果:
t1线程: i = 0
t1线程: i = 1
t1线程: i = 2
t1线程: i = 3
t1线程: i = 4
t1线程: i = 5
t2线程: j = 10
t2线程: j = 11
t2线程: j = 12
t2线程: j = 13
t2线程: j = 14
t2线程: j = 15
t1线程当前的状态为:WAITING
t2线程当前的状态为:WAITINGk
(2)notify
方法:可以在等待池中,随机唤醒一个等待指定锁对象的线程,使得这个线程进入到锁池中,而进入到锁池的线程, 一旦发现锁可用,就可以自动恢复到RUNNABLE
状态了
public class Test {
public static void main(String[] args) {
final Object obj = new Object();
Thread t1 = new Thread("t1"){
@Override
public void run() {
String name = Thread.currentThread().getName();
synchronized (obj){
for (int i = 0; i < 10; i++) {
System.out.println(name+"线程: i = "+i);
if(i==5){
try {
//在释放锁对象之前,叫醒等待池中等待obj锁对象的线程
//意思是告诉对方,我要释放锁了,你准备去抢把
obj.notify();
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//最后在执行完所有代码之前,再叫醒一次,防止等待池中还有其他线程在等待 obj这个锁对象
obj.notify();
}
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() {
String name = Thread.currentThread().getName();
synchronized (obj){
for (int j = 10; j < 20; j++) {
System.out.println(name+"线程: j = "+j);
if(j==15){
try {
//在释放锁对象之前,叫醒等待池中等待obj锁对象的线程
//意思是告诉对方,我要释放锁了,你准备去抢把
obj.notify();
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//最后在执行完所有代码之前,在叫醒一次,防止等待池中还线程在等待obj这个锁对象
obj.notify();
}
}
};
t1.start();
t2.start();
//主线程休眠1秒钟,给t1和t2点时间,等它们调用wait方法
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1线程当前的状态为:"+t1.getState());
System.out.println("t2线程当前的状态为:"+t2.getState());
}
}
//运行结果:
t1线程: i = 0
t1线程: i = 1
t1线程: i = 2
t1线程: i = 3
t1线程: i = 4
t1线程: i = 5
t2线程: j = 10
t2线程: j = 11
t2线程: j = 12
t2线程: j = 13
t2线程: j = 14
t2线程: j = 15
t1线程: i = 6
t1线程: i = 7
t1线程: i = 8
t1线程: i = 9
t2线程: j = 16
t2线程: j = 17
t2线程: j = 18
t2线程: j = 19
t1线程当前的状态为:TERMINATED
t2线程当前的状态为:TERMINATED
(3)notifyAll()
方法:可以在等待池中,唤醒所有等待指定锁对象的线程,使得这个线程进入到锁池中,而进入到锁池的线程, 一旦发现锁可用,就可以自动恢复到RUNNABLE状态了.
十七、死锁
- 在程序中要尽量避免出现死锁情况,一旦发生那么只能手动停止JVM的运行,然后查找并修改产生死锁的问题代码
- 死锁就是:俩个线程t1和t2,t1拿着t2需要等待的锁不释放,而t2又拿着t1需要等待的锁不释放,俩个线程就这样一直僵持下去。
public class ThreadDeadLock extends Thread{
private Object obj1;
private Object obj2;
public ThreadDeadLock(Object obj1,Object obj2) {
this.obj1 = obj1;
this.obj2 = obj2;
}
public void run() {
String name = Thread.currentThread().getName();
if("Thread-0".equals(name)){
while(true){
synchronized (obj1) {
synchronized (obj2) {
System.out.println(name+" 运行了..");
}
}
}
}else{
while(true){
synchronized (obj2) {
synchronized (obj1) {
System.out.println(name+" 运行了..");
}
}
}
}
}
public static void main(String[] args) {
Object obj1 = new Object();
Object obj2 = new Object();
Thread t1 = new ThreadDeadLock(obj1,obj2);
Thread t2 = new ThreadDeadLock(obj1,obj2);
t1.start();
t2.start();
}
}