Java基础(线程方法,线程同步,死锁)
1 sleep方法
线程类Thread中的sleep
方法:
public static native void sleep(long millis) throws InterruptedException;
该静态方法可以让当前执行的线程暂时休眠指定的毫秒数:
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
.....
可以看出,线程执行了sleep方法后,会从RUNNABLE状态进入到TIMED_WAITING状态
这时候线程所处的是一种阻塞状态,是之前介绍过的三种阻塞情况的其中一种
这种阻塞的特点是:阻塞结束后,线程会自动回到RUNNABLE状态
此时的状态图为:
2 join方法
线程类Thread中的join
方法:
public final synchronized void join(long millis)throws InterruptedException{
//...
}
public final void join() throws InterruptedException{
//...
}
使用join方法,可以让当前线程阻塞,等待另一个指定的线程运行结束后,当前线程才可以继续运行:
例如,使用无参的join方法
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线程结束
t2线程中,调用了t1对象的join方法,那么t2线程就会阻塞,等待t1线程的运行结束,t2线程才能恢复
可以看出,线程执行了join()方法后,会从RUNNABLE状态进入到WAITING状态
但是如果线程中调用的是有参数的join方法,线程所处的状态就不一样了:
例如,使用有参的join方法,其他代码和上面一样
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线程结束
可以看出,线程执行了join(long million)方法后,会从RUNNABLE状态进入到TIMED_WAITING状态
此时的状态图为:
线程A中,调用了线程B对象的join方法,那么线程A就会进入到阻塞状态,这种阻塞又俩种情况,一种是调用了无参的join方法,那么此时线程A的状态为WAITING(无限期等待),另一种是调用了有参的join方法,那么此时线程A的状态为TIMED_WAITING(有限期等待)
线程A如果调用了sleep方法,那么线程A也会进入阻塞状态,此时线程A的状态为TIMED_WAITING
总结:
- 如果指定了时间,线程阻塞一定的时间后,会自动恢复到RUNNABLE状态,这种情况下,线程的状态为TIMED_WAITING(有限期等待)
- 如果没有指定时间,线程会一直阻塞着,直到某个条件满足时,才会自动恢复,这种情况下,线程的状态为WAITING(无限期等待)
在这种情况下,其实还有另一种方式,可以让线程从阻塞状态恢复到RUNNABLE状态,那就是调用线程的
interrupt
方法
3 interrupt方法
线程类Thread中的interrupt
方法:
//Interrupts this thread
public void interrupt(){
//...
}
根据上面介绍sleep
方法和join
方法可知,这俩个方法都会抛出InterruptedException
类型的异常,说明调用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线程结束
可以看出,本来t1线程调用了sleep方法进入了阻塞状态,需要100后才会恢复的,但是我们在主线程中调用了t1线程对象的打断方法
interrupt
,那么此时Thread.sleep(100000);
这句代码就会抛出被打断的异常,同时t1线程从阻塞状态恢复到RUNNABLE状态,继续执行代码,输出了t1线程结束
此时对于的流程图为:
interrupt方法的工作原理:
interrupt
方法是通过改变线程对象中的一个标识的值(true|false),来达到打断阻塞状态的效果。
一个线程在阻塞状态下,会时刻监测这个标识的值是不是true,如果一旦发现这个值变为true,那么就抛出异常结束阻塞状态,并再把这个值改为false。
从Thread类的源码中可以看到:
interrupt
方法中其实是调用了interrupt0
这个本地方法,而interrupt0的注释为:Just to set the interrupt flag
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
private native void interrupt0();
可以看出,interrupt方法只是改变了线程对象中一个标识flag的值
查看线程对象中“打断标识”值的俩个方法:
线程类Thread中的isInterrupted
方法:
public boolean isInterrupted() {
return isInterrupted(false);
}
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
注意,这个非静态方法,只是返回这个“打断标识”值,并且不会对这个值进行清除(true->false),因为所传参数ClearInterrupted的值为false
例如,默认情况下,一个线程对象中的“打断标识”值为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线程结束
无论线程是否处于阻塞状态,其他线程都可以调用这个线程的
interrupt
方法,因为该方法只是改变线程对象中“打断标识”值而已
例如,
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();
t1.interrupt();
}
}
//运行结果:
true
true
true
true
true
true
true
true
true
true
t1线程结束
可以看出,吊用了
t1.interrupt();
后,t1线程中的“打断标识”值设置为了true,可以通过线程对象中的isInterrupted
方法返回这个标识的值,并且不会修改这个值,所以输出显示的一直是ture
线程类Thread中的interrupted
方法:
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
注意,这个静态方法,返回这个“打断标识”值,并且会对这个值进行清除(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线程结束
可以看出,第一次返回true之后,后面在调用方法查看这个“打断标识”值,都是false,因为静态方法
interrupted
返回true后,会直接把这个值给清除掉。(true->false)
Thread类中的三个方法:interrupt()、isInterrupted()、interrupted()
的结构关系大致如下:
public class Thread{
public void interrupt() {
//...
interrupt0(); // Just to set the interrupt flag
}
private native void interrupt0();
public boolean isInterrupted() {
return isInterrupted(false);
}
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
public static native Thread currentThread();
}
根据上面的代码结构,还有前面的一些示例以及解释,这个三个名字很像的方法,就应该容易分清楚了。
4 线程安全
JVM内存中的堆区,是一个共享的区域,是所有线程都可以访问的内存空间。
JVM内存中的栈去,是线程的私有空间,每个线程都有自己的栈区,别的先无法访问到自己栈区的数据。
我们之前编写的代码只有一个main线程,只有它自己去访问堆区中对象的数据,自然不会有什么问题。
但是在多线程环境中,如果有俩个线程并发访问堆区中一个对象中的数据,那么这个数据可能会出现和预期结果不符的情况,例如
public class Test {
public static void main(String[] args) {
final 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);
}
}
};
t1.start();
}
}
class MyData{
int num;
}
//运行结果:
t1: 0
t1: 1
t1: 2
t1: 3
t1: 4
t1: 5
t1: 6
t1: 7
t1: 8
t1: 9
这时候,t1线程自己访问堆区中的myData对象里面的num数据值,程序的结果每次都是和预期的一样
但是,如果再加一个线程t2,同时也访问这个堆区中myData对象中的num属性值,结果可能和预期的不一样:
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
可以看到,每次运行的结果中,t1线程输出的num的值可能和预期都不一样
此时的内存图为:
注意1,t1和t2并发访问的时候,争夺CPU的时间片,运行完时间片,退出后再次争夺下一个时间片,也就是说t1和t2都是“断断续续”的运行的
注意2,在这期间,可能t1线程有一次拿到时间片运行的时候,给num赋值为1,然后时间片用完退出了,结果下次t2线程拿到了时间片,又将num的值赋成了11750,然后t1线程又拿到了时间片,本来预期的是输出1,但是结果却是输出了11750
核心的原因是,t1线程操作一下变量num,然后时间片用完退出去,t2先过来又操作了变量num,等t1线程再过来的时候,这值已经被t2线程给“偷偷”修改了,那么就出现了和预期不符的情况
总结:
如果有多个线程,它们在一段时间内,并发访问堆区中的同一个变量,并且有写入的操作,那么最终可能会出数据的结果和预期不符的情况,这种情况就是线程安全问题。
我们经常会进行这样的描述:这段代码是线程安全的,那段代码是非线程安全的。 其实就是在说,这段代码在多线程并发访问的环境中,是否会出现上述情况,也就是结果和预期不符的情况。
例如,观察下面代码,是线程安全的还是非线程安全的? t1线程每次输出的结果是否有出现和预期不符的情况?
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread("t1"){
@Override
public void run() {
String name = Thread.currentThread().getName();
//每次循环改变变量i的值
for (int i = 0; i < 10; i++) {
//输出变量i的值
System.out.println(name + ": " + i);
}
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() {
//每次循环改变变量i的值
for (int i = 100; i < 20000; i++) {
}
}
};
t1.start();
t2.start();
}
}
每次执行结果都是一样的,和预期的相同,因为t1和t2俩个线程根本就没访问同一个共享变量!相当于t1和t2都是各自操作各自的变量i
思考,方法中的局部变量和对象中的成员变量分别在内存中什么地方?哪些变量可能被多个线程共享?
思考,如果t1和t2先不是“争先恐后”的并发访问变量num,而是排好队一个执行完,另一个在执行,是不是就可以解决这个线程安全问题了呢?
5 线程同步
当使用多个线程访问同一个共享变量的时候,并且线程中对变量有写的操作,这时就容易出现线程安全问题。
Java中提供了线程同步的机制,来解决上述的线程安全问题。
Java中实现线程同步的方式,是给需要同步的代码进行synchronized
关键字加锁。
例如,改造之前有线程安全问题的代码,给需要同步的代码使用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;
}
现在运行这个程序,每次输出的结果都是和预期的一样:t1线程每次输出的值都是0~9
分析:
线程同步的效果,就是一段加锁的代码,每次只能有一个拿到锁的线程,才有资格去执行,没有拿到的锁的线程,只能等拿到锁的线程把代码执行完,再把锁给释放了,它才能去拿这个锁然后再运行代码。
这样以来,本来这段代码是俩线程并发访问,“争先恐后”的去执行的,现在线程同步之后,这段代码就变成了先又一个拿到的锁的先去执行,执行完了,再由另一个线程拿到锁去执行。
相当于是大家每个线程不要抢,排好队一个一个去执行,那么这时候共享的变量的值,肯定不会出现线程安全问题!
例如,synchronized
修饰代码块的使用格式为:
synchronized (锁对象){
//操作共享变量的代码,这些代码需要线程同步,否则会有线程安全问题
//...
}
对应这样加锁的代码,如果俩个线程进行并发访问的话:
- 假设线程t1是第一个这段代码的线程,那么它会率先拿到这把锁,其实就是在这个锁对象中写入自己线程的信息,相当于告诉其他线程,这把锁现在是我的,你们都不能使用。
- 这时候t1线程拿着锁,就可以进入到加锁的代码块中,去执行代码,执行很短的一个时间片,然后退出,但是锁并不释放,也就意味着,及时下次是t2线程抢到CPU的使用权,它也无法运行代码,因为t2没有拿到锁。
- 就这样,t1线程开心的拿着锁,抢到CPU的执行权,抢到了就去执行,抢不到也不用担心,因为没有其他线程可以“偷偷”的执行这段代码,因为其他线程拿不到锁。
- 而对于t2线程来说,即使有一次抢到了CPU执行权,来到了代码面前,要执行的时候才发现,锁被t1线程拿走了,自己无法进入代码块中执行,这时候t2线程就会从运行状态进入阻塞状态,直到t1运行完,把锁释放了,t2线程才会恢复到RUNNABLE状态,抢到CPU执行权,再拿到锁,然后进入代码块中执行
注意,这时候t2线程的阻塞状态,和之前学习的调用sleep或join方法进入的阻塞不同,这种阻塞属于锁阻塞,需要等待另一个线程把锁释放了,t2线程才能恢复。如果t2线程处于这种阻塞,那么调用线程对象的getState
方法返回的状态名称为: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
注意1,t1线程需要拿到锁对象obj,才能运行加锁的代码块
注意2,t2线程也需要拿到锁对象obj,才能运行加锁的代码块
注意3,锁对象obj只有一个,所以t1和t2只能有一个线程先拿到,拿到后执行代码,那么另一个就拿不到了,拿不到就阻塞,此时线程的状态为:BLOCKED
注意4,java中,任意一个对象,只要是对象,就可以用来当做,加锁代码块中的锁对象。然后让多个线程去抢这拿这把锁就可以了,此时就达到了线程同步的效果,因为拿到的锁线程能执行代码,其他拿不到的线程就不执行,并且进入阻塞状态。
此时的线程状态图为:
可以看出,假设t1线程拿到了锁,t2线程没拿到锁,那么t2线程就会因为锁不可用,进入到锁阻塞状态,直到t1先把加锁的代码执行完,把锁释放了,锁变的可用了,这是t2线程会自动恢复到RUNNABLE状态
注意,t1线程“拿到”锁,只是一种形象的说,就是我们之前说的 引用“指向”对象一样。其实是t1线程把自己的信息写入到了锁对象中,用这种方式告诉其他线程,这个锁对象已经被我 "拿走"了
6 synchronized
通过上面线程同步例子的讲解,我们已经知道了synchronized
修饰一个代码块,并指定谁是锁对象的用法,除此之外,还可以使用synchronized
直接修饰一个方法,表示这个方法中的所有代码都需要线程同步。
synchronized
关键字修饰非静态方法,默认使用this
当做锁对象,并且不能自己另外指定
synchronized
关键字修饰静态方法,默认使用当前类的Class对象
当做锁对象,并且不能自己另外指定
这俩中情况的同步效果是一样的,只是锁对象不同而已
例如,
public class MyData{
private int[] arr = new int[20];
//当前数据可以存放的位置,也表示当前存放的元素个数
private int current;
//添加数据
public void add(int num){
String name = Thread.currentThread().getName();
arr[current] = num;
System.out.println(name+"线程本次写入的值为"+num+",写入后取出的值为"+arr[current]);
current++;
}
}
注意,MyData类中的add方法,在多线程并发访问的环境中,是有线程安全问题的
例如,
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();
}
}
//运行结果:
t1线程本次写入的值为0,写入后取出的值为0
t2线程本次写入的值为10,写入后取出的值为10
t2线程本次写入的值为11,写入后取出的值为1
t1线程本次写入的值为1,写入后取出的值为1
t2线程本次写入的值为12,写入后取出的值为2
t1线程本次写入的值为2,写入后取出的值为2
t2线程本次写入的值为13,写入后取出的值为3
t1线程本次写入的值为3,写入后取出的值为3
t2线程本次写入的值为14,写入后取出的值为4
t1线程本次写入的值为4,写入后取出的值为4
t1线程本次写入的值为5,写入后取出的值为5
t2线程本次写入的值为15,写入后取出的值为5
t2线程本次写入的值为16,写入后取出的值为16
t1线程本次写入的值为6,写入后取出的值为16
t1线程本次写入的值为7,写入后取出的值为17
t2线程本次写入的值为17,写入后取出的值为17
t1线程本次写入的值为8,写入后取出的值为18
t2线程本次写入的值为18,写入后取出的值为18
t1线程本次写入的值为9,写入后取出的值为19
t2线程本次写入的值为19,写入后取出的值为19
可以看出,在某一次add方法执行的时候,会出现写入的数据和当前的数据不一致的情况
此时,我们可以直接在add方法(非静态方法)上,添加修饰符synchronized
关键字,表示给这个方法中的所有代码进行线程同步,默认使用的锁对象是this
public synchronized void add(int num){
String name = Thread.currentThread().getName();
arr[current] = num;
System.out.println(name+"线程本次写入的值为"+num+",写入后取出的值为"+arr[current]);
current++;
}
此时,再运行代码,就不会出现之前那种线程安全问题了
该代码表示,拿到锁对象this的线程,才可以进入到add方法中执行代码,代码执行完,会释放锁,这时锁变的可用了,所有需要这把锁的线程都恢复到RUNABLE状态(它们之前在锁阻塞状态),这些线程一起重新争夺CPU执行权,谁先拿到CPU执行权,就会先过去拿到锁,进入代码去执行
注意,此时t1线程中调用add方法,争夺的锁对象this就是myData对象,t2线程中调用的add方法,争夺的锁对象this也是myData对象,所以t1和t2俩个线程争夺的是同一把锁对象,那么就能达到线程同步的效果!
所以,线程同步的效果的关键点在于,让t1和t2俩个线程去争夺同一把锁对象
思考,根据上面的例子,考虑为什么之前学习的ArrayList中的add方法是非线程安全的,而Vector中的add方法是线程安全的?
7 wait和notify
Object类中有三个方法:wait()、notify()、notifyAll
当一个对象,在线程同步的代码中,充当锁对象的时候,在synchronized
同步的代码块中,就可以调用这个锁对象的这三个方法了。
三个核心点:
- 任何对象中都一定有这三个方法
- 只有对象作为锁对象的时候,才可以调用
- 只有在同步的代码块中,才可以调用
其他情况下,调用一个对象的这三个方法,都会报错!
synchronized
关键字,虽然可以达到线程同步的效果,但是太“霸道”了,只要一个线程拿到了锁对象,那么这个线程无论是在运行状态,还是时间片用完,回到就绪状态,还是sleep休眠,这个线程都是死死的拿着这个锁对象不释放,只有这个线程把线程同步的代码执行完,才会释放锁对象让别的线程使用。
那么有没有一个方法,可以让拿到的锁的线程,即使代码没执行完,也可以把锁立即给释放了呢?
有的,这个就是wait
方法
例如,
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);
}
}
}
};
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);
}
}
}
};
t1.start();
t2.start();
}
}
t1和t2俩个线程,争夺同一把锁对象obj,所以程序的运行结果是:要么t1先拿到锁输处09,然后t2再拿到锁输出1019,要么就是t2先拿到锁输入1019,然后t1再拿到锁输出09
现在,我们希望的是t1线程中i=5的时候,先释放锁,让t2拿到锁去运行,在t2线程中,当j=15的时候,释放锁,让t1拿到锁去运行:
在代码中加入条件判断和wait方法的调用
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方法
//让当前拿到锁的线程,立即释放锁
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方法
//让当前拿到锁的线程,立即释放锁
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
};
t1.start();
t2.start();
}
}
//运行结果:
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线程和t2线程都没有运行完,但是代码不运行了,JVM也没停住
这是因为,当前调用锁对象的wait方法后,当前线程释放锁,然后进入到阻塞状态,并且等待其他线程先唤醒自己,如果没有其他线程唤醒自己,那么就一直等着。所以现在的情况是,俩个线程t1和t2都是在处于阻塞状态,等待别人唤醒自己,所以程序不运行了,但是也没结束!
此时,调用t1和t2的getState
方法,返回的状态为:WAITING
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线程当前的状态为:WAITING
此时线程的状态图为:
可以看出,此时线程调用了wait方法,释放了锁,变为阻塞状态(WAITING),并进入了等待池,等待其他线程唤醒自己或者打断自己,如果有线程调用了notify方法进行了唤醒,或者interrupt方法进行了打断,那么这个线程就会从等待池进入到锁池,而进入到锁池的线程,会时刻关注锁对象是否可用,一旦可用,这个线程就会立刻自动恢复到RUNNABLE状态。
由图可知,TIMED_WAITING、WAITING、BLOCKED都属于线程阻塞,他们共同的特点是就是线程不执行代码,也不参与CPU的争夺,除此之外,它们还有各自的特点:(重要)
- 阻塞1,线程运行时,调用sleep或者join方法后,进入这种阻塞,该阻塞状态可以恢复到RUNNABLE状态,条件是线程被打断了、或者指定的时间到了,或者join的线程结束了
- 阻塞2,线程运行时,发现锁不可用后,进入这种阻塞,该阻塞状态可以恢复到RUNNABLE状态,条件是线程需要争夺的锁对象变为可用了(别的线程把锁释放了)
- 阻塞3,线程运行时,调用了wait方法后,线程先释放锁后,再进入这种阻塞,该阻塞状态可以恢复到BLOCKED状态(也就是阻塞2的情况),条件是线程被打断了、或者是被别的线程唤醒了(notify方法)
理解上述的状态变化过程后,我们修改代码,加入notify方法的调用:
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
可以看到,此时t1和t2俩个线程都执行完了,打印输出的结果也符合我们的预期
锁对象.notify(),该方法可以在等待池中,随机唤醒一个等待指定锁对象的线程,使得这个线程进入到锁池中,而进入到锁池的线程, 一旦发现锁可用,就可以自动恢复到RUNNABLE状态了
锁对象.notifyAll(),该方法可以在等待池中,唤醒所有等待指定锁对象的线程,使得这个线程进入到锁池中,而进入到锁池的线程, 一旦发现锁可用,就可以自动恢复到RUNNABLE状态了
思考,线程调用无参的wait方法,会释放锁并进入等待池,且此时的状态为WAITING,如果线程调用有参的wait方法,指定一个等待时间,那么线程释放锁后,进入到等待池,此时的状态还是WAITING么?
那就不是的,如果指定了一个等待时间,那状态就是timed_waiting了,这两者的区别就是waiting是一定要通过别的方法唤醒它才能进入就绪状态,而timed_waiting只要时间到了自动变为就绪状态同其他线程争取时间片。
案例:
根据以下代码,补全pos和wit方法完成其功能
要求,银行账号的余额不能是负数
public class Account {
//账号余额
private int balance;
public Account(int balnace) {
this.balance = balnace;
}
//存钱
public void pos(int money){
}
//消费
public void wit(int money){
}
}
//男孩,负责挣钱
public class Boy extends Thread{
private Account account;
public Boy(Account account, String name) {
this.account = account;
setName(name);
}
public void run() {
//一直不停的挣钱
while(true){
//随机钱数
int money = (int)(Math.random()*10000+1);
//存进账户
account.pos(money);
try {
//休息1秒后,继续挣钱
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//女孩,负责花钱
public class Girl extends Thread{
private Account account;
public Girl(Account account, String name) {
this.account = account;
setName(name);
}
public void run() {
//一直不停的花钱
while(true){
//随机钱数(看心情)
int money = (int)(Math.random()*10000+1);
//刷卡消费
account.wit(money);
try {
//休息1秒后,继续花钱
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class AccountTest {
public static void main(String[] args) {
Account account = new Account(5000);
Boy boy = new Boy(account, "tom");
Girl lily1 = new Girl(account, "lily1");
Girl lily2 = new Girl(account, "lily2");
Girl lily3 = new Girl(account, "lily3");
boy.start();
lily1.start();
lily2.start();
lily3.start();
}
}
8 死锁
在程序中要尽量避免出现死锁情况,一旦发生那么只能手动停止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();
}
}
注意,可以通过jconsole查看到线程死锁的情况