概念
进程与线程
-
进程
一段运行的程序,一个应用程序就是一个进程,如QQ、浏览器、网易云音乐,但进程不一定就是应用程序,有可能运行在后台,具体定义可以查看百度或者计算机操作系统相关的书籍。 -
线程
线程是独立调度的最小单位,一个进程中至少有一个线程,即主线程。当一个进程没有开启其他线程,即单线程,代码顺序执行,若开启了其他线程,则CPU会轮流调度每个线程,即每个线程的代码交替执行。
比如微信在接收文件时,我们还可以输入文字并发送给对方,这里涉及3个线程,一个负责接收文件,一个负责响应UI操作(点击输入框打字),一个负责发送信息,这三个线程轮流占用CPU,让我们看起来是同时的。
并行与并发
-
并行
并行是指两件事情在同一时刻发生,比如两个CPU同时执行一段代码,是真正意义上的同时。 -
并发
并发是在一个时间段内发生两件事情,但有先后顺序,不是真正的同时,CPU调度线程的过程就是并发,只不过它们轮换CPU的速度很快,我们察觉不了,看起来是多个线程同时执行,所以多线程编程也叫并发编程。
同步和异步
-
同步
同步并不是两件事同时发生,而是多个事情的发生有确定的次序,线程同步是指线程的执行有明确的顺序,通常是通过加锁来实现。 -
异步
相对于同步,异步是指多个事件的发生次序不确定,线程如果不加控制就是异步的,但异步的无序性造成了代码运行的不确定性,所以要人为的控制线程,使线程同步。
线程的状态转换(生命周期)
新建(New)
线程也是一个对象,使用对象就要先实例化,刚实例化的线程就处于新建状态。
可运行状态(Runnable)
刚创建的线程调用Thread.start()后就变成可运行状态,这个状态包括了ready和running的线程,running是指正在占用CPU资源的线程,ready是等待被CPU调度的线程。
阻塞(Blocked)
线程在运行时如果被加上了锁(Synchronized或Lock),线程就会进入阻塞状态,其他线程释放掉锁,阻塞的线程才会重新回到可运行状态。
限时等待(Time Waiting)
运行中的线程调用Thread.sleep()或带参数的Object.wait()等方法会进入限时等待状态,进入等待状态通常被称为“线程睡眠”,等事件结束或其他线程显式调用Object.notify()或Object.notifyAll()方法,该状态的线程会被唤醒,即变回可运行状态。
进入方法 | 退出方法 |
---|---|
Thread.sleep()方法 | 时间结束 |
设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() |
设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 |
无限等待(Waiting)
运行中的线程调用Object.wait()方法或调用了其他线程的join()方法,会使该线程进入无限等待状态,在这个状态中只能等待其他线程显式地唤醒,否则不能被CPU调度。
进入方法 | 退出方法 |
---|---|
没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
死亡(Terminated)
当线程调用start()方法开启后,无论在哪个状态中,只要抛出异常,线程就会终结,即进入死亡状态。当然,线程正常执行完任务后也会终结。
线程的使用
继承Thread类
继承Thread类并重写run()方法。
public class MyThread extends Thread {
public void run() {
System.out.println("继承Thread类的线程运行!");
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
实现Runnable接口
实现Runnable接口,给出run()方法的实现,实例化Runnable对象传递给Thread,调用start()方法。
public class MyRunnable implements Runnable {
public void run() {
System.out.println("实现Runnable接口的线程运行!");
}
}
public static void main(String[] args) {
Thread myThread = new Thread(new MyRunnable());
myThread.start();
}
实现Callable接口
相对于Runnable,Callable可以有返回值,返回值通过通过 FutureTask 进行封装。
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
日常开发中,前两种方式用得比较多。无论是哪种方式,最后都要通过调用Thread.start()来启动线程。
实现接口 VS 继承Thread类
推荐通过实现接口来开启线程,因为java不支持多继承,如果继承Thread类就无法继承其他类,但接口可以实现多个。
线程的常用方法
Thread.sleep()
这个方法接收一个long型的参数,表示毫秒,比如Thread.sleep(3000)代表调用该方法的线程进入等待状态,3秒后再进入可运行状态。看一下例子(因为只装了Android Studio,就用AS来测试了)
private class MyTask1 implements Runnable {
@Override
public void run() {
try {
Log.d(getClass().getName(), "run: 睡眠3秒");
Thread.sleep(3000);
Log.d(getClass().getName(), "run: 睡眠结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_thread_sleep);
Thread thread = new Thread(new MyTask1());
thread.start();
}
可以看到两条日志的时间相差3秒(还有2ms可以忽略)。需要注意的是,Thread.sleep()方法有可能抛出异常,而异常又不能跨线程抛给其他线程,所以要用try-catch语句在本线程处理该异常。另外,这个方法还有一个接收两个参数的,public static void sleep(long millis, int nanos)
,第二个参数表示额外等待的纳秒时间,日常开发中用毫秒的就够了,除非对时间有特别严谨的要求。
Thread.yield()
yield的意思是放弃、让步,若某线程调用该方法,则表示该线程已完成某些重要的步骤,建议CPU调度其他线程执行。
public void run() {
Thread.yield();
}
线程的中断
InterruptedException
通过调用指定线程的interrupt()方法,如果该线程处于阻塞、限时等待或无限等待状态,那么会抛出InterruptedException,从而提前结束该进程。看个例子。
private class MyTask1 implements Runnable {
@Override
public void run() {
try {
Log.d(getClass().getName(), "run: 线程1睡眠");
Thread.sleep(5000);
Log.d(getClass().getName(), "run: 线程1睡眠结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_interrupted);
Thread threadA = new Thread(new MyTask1());
threadA.start();
threadA.interrupt();
}
可以看到线程1本来要睡眠5秒,但在主线程中调用了线程1的interrupt()方法,因为线程1在限时等待状态,所以抛出了异常,导致线程提前中断。
interrupt()
如果线程没有使用sleep()、join()、wait()等有可能会抛出InterruptedException异常的方法,那么调用线程的interrupt()不能让线程中断,但是interrupt()会让该线程设置一个中断标志,线程内部调用interrupted() 方法会返回 true,所以线程内部可以用循环判断该线程是否可中断,从而提前结束线程。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_interrupted);
MyThread myThread = new MyThread();
myThread.start();
try {
// 主线程睡眠5ms再给子线程设置中断标记,让子线程有机会执行
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
myThread.interrupt();
}
private class MyThread extends Thread{
@Override
public void run() {
while (!isInterrupted()) {
Log.d(getClass().getName(), "run: 线程运行");
}
Log.d(getClass().getName(), "run: 跳出循环");
}
}
注意,这种方法只能通过继承Thread类使用,因为isInterrupted()是Thread类内部的方法。
线程同步
为什么要让线程同步
这个问题相当于为什么不能让线程异步,上面提到线程的异步具有不确定性,就是通常说的线程不安全。什么意思?举个例子。
public class ThreadNotSafeActivity extends AppCompatActivity {
private volatile int count = 0;
private volatile boolean isThreadADone = false;
private volatile boolean isThreadBDone = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_thread_not_safe);
new Thread(new Runnable() {
@Override
public void run() {
for (int i=1;i<=500000;i++){
count++;
}
isThreadADone = true;
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i=1;i<=500000;i++){
count++;
}
isThreadBDone = true;
}
}).start();
while(!isThreadADone || !isThreadBDone){
}
Log.d(getClass().getName(), "run: count最终值" + count);
}
}
这段代码很简单,就是两个线程各自循环50万次,递增count变量,预期的结果应该是count最后等于1000000,而这次运行的结果903248,而且同一条件下再运行一次,结果又不一样了,这就是线程不安全的例子。如果放到银行的系统中,两个人同时存50万(虽然不可能每次存一块钱),但是银行收到的钱却不是100万,这是绝对不允许出现的情况。
为什么会出现这种情况?就上面的例子来看,count++
不是一个原子操作。那什么是原子操作呢?(关于原子性、可见性、重排序的知识又是一个专题,之后再写总结,如果不懂的可以看一下别人关于这方面写的博客,最好是结合书本系统学习,比如《深入理解java虚拟机》和《java并发编程的艺术》)
通俗地说,原子操作是指线程在做这个操作时,其他线程不能打断。例如count++
就包含了3个原子操作:
1)从内存读取count的值
2)count + 1
3)将count的值写回到内存中
这就是上面线程不安全的主要原因,比如出现一种情况,当前count的值为100,当线程A读取count的值并加1,在写回内存之前线程B也读取count的值(此时内存中count还是100),然后加1,写回内存,此时count等于101,但是线程A中此时也把count的值(101)写回内存,这样执行了两次加1操作,而count最终的值却是101,不符合预期的结果。
因此,对于有依赖关系的线程或访问同一变量的线程,我们要让这些线程同步。Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。
synchronized
同步代码块
public void func() {
synchronized (this) {
// ...
}
}
这种方法只作用在一个对象上,调用不同对象的同步代码块,不会进行同步。如例子所示。
private class MyClass {
public void fun(){
synchronized (this) {
for (int i=1;i<=10;i++)
Log.d(getClass().getName(), "fun: " + i);
}
}
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_synchronized_sample);
final MyClass myClass = new MyClass();
new Thread(new Runnable() {
@Override
public void run() {
myClass.fun();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
myClass.fun();
}
}).start();
}
上面的代码使两个线程同步,一个线程循环结束后,另一个线程才可以进入循环,但这只能同步一个对象,如果两个线程操作不同的对象则不会同步。把上面代码改一下:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_synchronized_sample);
final MyClass myClass = new MyClass();
final MyClass myClass1 = new MyClass();
new Thread(new Runnable() {
@Override
public void run() {
myClass.fun();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
myClass1.fun();
}
}).start();
}
同步一个方法
public synchronized void func () {
// ...
}
跟同步代码块一样,作用于一个对象。
同步一个类
public void func() {
synchronized (MyClass.class) {
// ...
}
}
作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。
private class MyClass {
public void funA(){
synchronized (MyClass.class) {
for (int i=1;i<=100;i++)
Log.d(getClass().getName(), "fun: " + i);
}
}
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_synchronized_sample);
final MyClass myClass = new MyClass();
final MyClass myClass1 = new MyClass();
new Thread(new Runnable() {
@Override
public void run() {
myClass.funA();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
myClass1.funA();
}
}).start();
}
可以看到两个对象同步了,一个线程循环结束另一个线程才进入循环。
同步一个静态方法
public synchronized static void fun() {
// ...
}
作用于整个类。
ReentrantLock
ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。用法如下:
public class ReentrantLockExampleActivity extends AppCompatActivity {
private Lock lock = new ReentrantLock();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_reentrant_lock_example);
final MyClass myClass = new MyClass();
final MyClass myClass1 = new MyClass();
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
myClass.fun();
}finally {
// 确保解锁,避免死锁
lock.unlock();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
myClass1.fun();
}finally {
// 确保解锁,避免死锁
lock.unlock();
}
}
}).start();
}
private class MyClass {
public void fun() {
for(int i=1;i<=100;i++)
Log.d(getClass().getName(), "fun: "+ i);
}
}
}
lock.lock()的作用是尝试获得锁,如果获得锁,则进入同步语句块,若该锁被其他线程获取,则进入阻塞状态等待其他线程释放该锁;lock.unlock()的作用就是释放锁,让其他被阻塞的线程可以获得锁。
这里只是介绍了ReentrantLock的简单用法,还有很多更加高级的功能,大家可以查阅一下(以后我也会自己写一篇总结)。要注意的是lock和unlock要成对使用,避免死锁。
死锁
死锁是多线程编程中的一个重要概念,通俗地讲,就是线程之间要获取对方的资源才能释放自身占用的资源,这样就进入一个死循环,导致线程不能执行下去。锁能让线程同步,但也有可能造成死锁,所以我们要避免死锁的发生。改一下上面的代码:
public class ReentrantLockExampleActivity extends AppCompatActivity {
private Lock lock = new ReentrantLock();
private Lock lock1 = new ReentrantLock();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_reentrant_lock_example);
final MyClass myClass = new MyClass();
final MyClass myClass1 = new MyClass();
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock1.lock();
try {
myClass.fun();
}finally {
// 确保解锁,避免死锁
lock1.unlock();
lock.unlock();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
lock1.lock();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
try {
myClass1.fun();
}finally {
// 确保解锁,避免死锁
lock.unlock();
lock1.unlock();
}
}
}).start();
}
private class MyClass {
public void fun() {
for(int i=1;i<=100;i++)
Log.d(getClass().getName(), "fun: "+ i);
}
}
}
上面就是会发生死锁的例子,线程A获得锁lock后睡眠3秒,此时线程B获得锁lock1,也睡眠3秒,等睡眠结束时,线程A和线程B都尝试获取对方占有的锁,于是两个线程都进入阻塞,始终无法释放占有的资源,进入死锁,所以无论等多久,也不会有日志打印出来。
关于死锁的知识还有很多,如死锁的4个必要条件、避免死锁的方法等,这里只简单介绍死锁的概念。
synchronized VS ReentrantLock
1.锁的实现
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
2.性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
3.等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。ReentrantLock 可中断,而 synchronized 不行。
4.公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
5. 锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象。
使用选择
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
线程协调
join()
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,直到目标线程结束。
public class JoinExampleActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_join_example);
final Thread threadA = new Thread(){
@Override
public void run() {
Log.d(getClass().getName(), "run: 线程A执行");
}
};
Thread threadB = new Thread(){
@Override
public void run() {
Log.d(getClass().getName(), "run: 先让线程A执行");
try {
threadA.join();
Log.d(getClass().getName(), "run: 线程B执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
threadB.start();
threadA.start();
}
}
wait()、notify()、notifyAll()
调用 wait() 使得线程等待某个条件满足(即进入无限等待状态),线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。
这三个方法只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateException(即要和synchronized搭配使用)。
它们都属于 Object 的一部分,而不属于 Thread。这些方法的作用是改变线程的状态,为什么不属于Thread而属于Object?
我的理解是,这些方法不仅可以改变线程状态,更重要的是这些方法也会对锁进行操作,比如调用wait()会释放synchronized,让其他阻塞的线程进入同步语句块(所以这些方法一定要在synchronized语句内使用),而synchronized就是在对象上加锁(用synchronized修饰方法或语句块其实就是给所属对象加锁),那解锁当然是解某个对象上的锁,所以这些方法放在Object里是合理的。
按照惯例,举个例子。
public class WaitNotifyExampleActivity extends AppCompatActivity {
private Object object = new Object();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_wait_notify_example);
Thread threadA = new Thread(){
@Override
public void run() {
synchronized (object){
try {
object.wait();
Log.d(getClass().getName(), "run: 线程A运行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread threadB = new Thread(){
@Override
public void run() {
synchronized (object){
Log.d(getClass().getName(), "run: 线程B运行");
object.notify();
}
}
};
threadA.start();
try {
// 主线程睡眠1秒再开启线程B
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadB.start();
}
}
notify()随机唤醒一个线程,notifyAll()唤醒所有线程,前提是这些线程都在等同一把锁,等待其他锁的线程不会被唤醒。
wait()和sleep()的区别
- wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
- wait() 会释放锁,sleep() 不会。
await() signal() signalAll()
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。
和Lock搭配使用,相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。
public class AwaitSignalExampleActivity extends AppCompatActivity {
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_await_signal_example);
Thread threadA = new Thread(){
@Override
public void run() {
lock.lock();
try {
conditionA.await();
Log.d(getClass().getName(), "run: 线程A被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};
Thread threadB = new Thread(){
@Override
public void run() {
lock.lock();
try {
conditionB.await();
Log.d(getClass().getName(), "run: 线程B被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
};
Thread threadC = new Thread(){
@Override
public void run() {
lock.lock();
Log.d(getClass().getName(), "run: 线程C运行");
conditionA.signalAll();
lock.unlock();
}
};
threadA.start();
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadC.start();
}
}
可以看到线程A和线程B开启后,就执行不同condition的await()方法,进入等待状态,主线程睡眠1秒后再开启线程C,在线程C中,调用conditionA的signalAll()方法,结果应该只有线程A被唤醒。
结果符合预期,可以推断出如果线程C中的conditionA.signalAll();
改成conditionB.signalAll();
,那么线程B会被唤醒,如果两句都有,则线程AB都会被激活。
wait() VS await()
- wait()、notify()、notifyAll()搭配synchronized使用,它们都属于Object的方法,await()、signal()、signalAll()搭配Lock和Condition使用,是Condition中的方法。
- notify()随机唤醒一个线程,signal()唤醒指定条件的线程,更加灵活。日常开发中,用synchronized和wait()、notify()、notifyAll()就可以应对大多数场景,且更安全(不会发生死锁),但如果想更精准的控制线程的调度,则使用另外一种。