2020-01-31-Java多线程
线程生命周期
线程生命周期.jpgNEW:创建了一个线程对象,但是还没有调用start()方法。此时称为初始状态NEW
RUNNABLE:调用了start()方法后,进入就绪状态,此时已经进入run()方法
BLOCKED:如果run()方法中有用synchronized包含的代码块,则需要等其他线程释放锁对象才能进入synchronized代码块。
BLOCKED状态是不能被interrupt打断的,只有其他线程释放锁之后,才能回到RUNNABLE状态。
WAITING:如果线程主动调用wait()或join()方法,则进入WAITING状态,
直到join()的线程返回;
或者wait()的对象执行notify()或notifyAll()方法被唤醒;
或者其他线程调用interrupt()方法。
如果没有其它线程唤醒,WATING状态会无限期等待下去,形成死锁。
TIMED_WAITING:如果线程主动调用sleep(long millis),wait(long millis),或join(long millis)方法,则进入TIMED_WAITING状态,
直到超时返回或join()的线程返回;
或者wait()的对象执行notify()或notifyAll()方法被唤醒;
或者其他线程调用interrupt()方法。
TERMINATED:如果run()方法执行完毕,或者线程异常退出,进入TERMINATED状态。进入此状态后不能再调用start()方法。
线程等待
- sleep()和wait()
sleep()没有锁的概念,wait()有。wait()方法会释放线程本身持有的锁。
sleep()方法会进入TIMED_WAITING,直到超时返回,跟锁没有关系,如果sleep()方法被synchronized代码块包含,线程本身持有的锁不会释放。
不带参数的wait()方法会进入WAITING状态,无限期等待,直到满足条件,或者interrupt()方法被调用。
带参数的wait(long millis)方法会进入TIMED_WAITING状态,能实现超时返回。 - sleep()和join()
sleep()和join()都没有锁的概念,只是等待返回的条件不一样。
不带参数的join()方法会进入WAITING状态,无限期等待,直到该线程返回,或者interrupt()方法被调用。
带参数的join(long millis)方法会进入TIMED_WAITING状态,能实现超时返回。 - sleep()和yield()
sleep()和yield()都没有锁的概念。
yield()方法不会改变线程的状态,只是让出CPU使用权。有可能下一次CPU会立即执行。
sleep()方法会让出CPU使用权,直到超时返回。 - sleep()和suspend()
suspend()方法不会改变线程状态,依然是RUNNABLE,但是被暂停了不会执行,只有通过resume()方法能够唤醒。suspend()和resume()已经不推荐使用了。
线程安全
多线程并发可能会遇到的问题是同样的输入,每次的输出不一样,需要了解Java内存模型(JMM)的几个概念:
- 原子性:操作中途是不能被其他线程打断的;
- 可见性:一个线程对共享变量的修改,其他线程是立即可见的;
-
有序性:在多核处理器的环境下,代码的执行顺序是没保障的,需要其他条件来保证。
在Java内存模型中,允许编译器和处理器对指令进行重排序。重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
内存模型.jpg
为了实现操作的原子性,可见性和有序性,通常会用到锁。
Java提供了synchronized关键字来实现内部锁。被 synchronized 关键字修饰的方法和代码块叫同步方法和同步代码块。
- synchronized 方法
锁的对象是每个类实例,只有两个线程对同一个类实例的synchronized方法进行访问才会竞争锁资源,分别对两个实例的同一个synchronized方法访问是不影响的。
例如下面这个例子:
当两个线程分别去访问同个对象的两个synchronized方法时,就可能出现时序问题。
如果count()方法先执行,两个线程都能够正常运行。
如果waitCount()方法先执行,就会出现死锁,waitCount线程进入死循环,一直在TIMED_WAITING状态,count线程没有获取到对象锁,一直在BLOCKED状态。
public class ThreadTest {
private volatile int count = 0;
public synchronized void count() {
for (int i = 0 ; i < 10; i++) {
count++;
}
System.out.println(Thread.currentThread().getName() + " count=" + count);
}
public synchronized void waitCount() {
System.out.println(Thread.currentThread().getName() + " wait start");
while (count < 10) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
System.out.println(Thread.currentThread().getName() + " wait finish");
}
}
ThreadTest test = new ThreadTest();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
test.waitCount();
}
}, "thread1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
test.count();
}
}, "thread2");
synchronized方法保证了操作的原子性,所以synchronized内部最好不要对共享变量使用一些while判断,防止出现死循环。
- synchronized代码块
synchronized代码块跟synchronized方法是类似的,只不过可以指定锁的对象,使用比较灵活,可以把尽可能少的操作放进同步代码,避免其他线程等待。
例如上面的例子,只需要把count()的同步方法改成同步代码块,就能解决死循环的问题。
public class ThreadTest {
private volatile int count = 0;
public void count() {
for (int i = 0 ; i < 10; i++) {
count++;
}
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " count=" + count);
}
}
public synchronized void waitCount() {
System.out.println(Thread.currentThread().getName() + " wait start");
while (count < 10) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
System.out.println(Thread.currentThread().getName() + " wait finish");
}
}
- 显式锁
常用的显式锁有ReentrantLock,实现了Lock接口的四个方法:
lock()和lock.unlock()方法类似synchronized,等待锁的过程中线程阻塞,只不过synchronized是进入BLOCKED状态,lock()是进入WAITING状态。
lockInterruptibly()可以被中断,需要catch InterruptedException异常。
tryLock()成功获取锁返回true,否则false,不会阻塞。
tryLock(long time, TimeUnit unit),阻塞等待一段时间,然后返回。
此外还有读写锁,这里不介绍,可以看下ReadWriteLock这个类。 - volatile
一般变量保存在高速缓存区,不会立刻同步到主内存,导致不同线程间的数据不同步,使用volatile关键字就是为了保证数据的可见性。
下面写了一个简单例子,两个线程thread1和thread2会竞争lock对象锁
package com.one.thread;
import java.util.Random;
import com.one.log.Log;
public class ThreadTest {
private volatile int count = 0;
private Log log = new Log("ThreadTest");
private Object lock = new Object();
public void run() {
thread1.start();
thread2.start();
}
private void sleepRandom() {
try {
Thread.currentThread().sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
log.log("sleep interrupt");
}
}
private Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
thread2.join(1000);
} catch (InterruptedException e) {
log.log("thread1 joinInterrupt");
}
for (int i = 0; i < 10; i++) {
synchronized (lock) {
sleepRandom();
count++;
log.log("count=" + count + " thread2=" + thread2.getState());
lock.notifyAll();
}
}
}
}, "thread1");
private Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
sleepRandom();
log.log("thread1=" + thread1.getState());
while (count < 6) {
try {
lock.wait();
sleepRandom();
log.log("waitting count=" + count+ " thread1=" + thread1.getState());
} catch (InterruptedException e) {
log.log("thread2 waitInterrupt");
break;
}
}
log.log("thread2 waitFinished");
}
}
}, "thread2");
}
这两个线程的状态如下
Thread.png
如果代码中的join()方法没有加超时,就会形成死锁。
参考:
https://www.cnblogs.com/dolphin0520/p/3920373.html
https://www.cnblogs.com/trust-freedom/p/6606594.html