目录
目录
1,Executors.newFixedThreadPool
2,Executors.newCachedThreadPool
3,Executors.newSingleThreadExecutor
4,Executors.newScheduledThreadPool
5,Executors.newSingleThreadScheduledExecutor
6,Executors.newWorkStealingPool
对于线程的理解一直有限,最近回看下线程,然后自己做个详细的记录吧!
一 线程和进程的区别
进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。
线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。
线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
多进程是指操作系统能同时运行多个任务(程序)。
多线程是指在同一程序中有多个顺序流在执行。
二 线程的状态,转换和调度。
1,线程状态
- New:代表已创建但还没启动的新线程,当我们用new Thread();新建一个线程之后,但是还没有执行start()方法,此时就处于New的状态;
- Runnable(可运行状态):从New,调用了start()方法之后,就会处于Runnable状态;Runnable有可能在执行,也有可能没有在执行(等待CPU分配运行时间);比如说一个线程拿到了CPU资源了————Runnable状态,CPU资源是被我们调度器不停的调度,所以有的时候,会突然被拿走,一旦我们某一个线程拿到了CPU资源,正在运行了,突然CPU资源被抢走分配给别人,此时这个线程还是Runnable状态;因为虽然它并没有在运行中,但它依然是处于可运行状态,随时随地都有可能又被调度器分配回来CPU资源,然后就又可以运行了;
- Blocked:当一个线程进入到被synchronized的代码块的时候,并且该锁(monitor)已经被其他线程所拿走了,此时的状态是Blocked;等待另外线程释放排它锁在进入可运行状态;
- Waiting(等待):与第三个状态类似;一方面是没有设置timeout参数的Object.wait()、Thread.join()、LockSupport.park() 直到被唤醒才可进入可运行状态;
- Timed Waiting(计时等待):和等待状态非常类似,有时间期限;Thread.sleep(time)、Object.wait(time)、Thread.join(time)、LockSupport.parkNanos(time)、LockSupport.parkUntil(time);
- Terminated(终止):,run()正常退出、出现未被捕获的异常;
一般习惯而言,把Blocked(被阻塞)、Waiting(等待)、Timed_waiting(计时等待)都称为阻塞状态,不仅仅是Blocked。
2,线程转换和调度
1)调整线程优先级
Java线程有优先级,优先级高的线程会获得较多的运行机会。Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
//线程可以具有的最高优先级,取值为10。
static int MAX_PRIORITY
///线程可以具有的最低优先级,取值为1。
static int MIN_PRIORITY
//分配给线程的默认优先级,取值为5。
static int NORM_PRIORITY
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
2)线程睡眠
Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。不会暂时释放锁。
3)线程等待
Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。会释放掉锁。
4)线程让步
Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。只是从运行中转化到可运行的状态,很可能在下一次的调用的时候,再次调用到此线程。会释放掉锁。
5)线程加入
Object类中的join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。join底层调用的是wait(),而wait是Object的方法,wait本身是会释放锁(彻底交出CPU的执行权),所以 Thread 的join() 方法是会释放锁。
6)线程唤醒
Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向。
7)线程终止
Object.interrupt():中断某个线程,这种结束方式比较粗暴,如果t线程打开了某个资源还没来得及关闭也就是run方法还没有执行完就强制结束线程,会导致资源无法关闭;要想结束进程最好的办法就是在线程类里面用以个boolean型变量来控制run()方法什么时候结束,run()方法一结束,该线程也就结束了。
Thread.currentThread().isInterrupted():测试当前线程是否被中断。线程的中断状态受这个方法的影响,意思是调用一次使线程中断状态设置为 true,连续调用两次会使得这个线程的中断状态重新转为 false;
Object.isInterrupted():测试当前线程是否被中断。与上面方法不同的是调用这个方法并不会影响线程的中断状态。
三 线程的三种实现方式
1,继承Thread类
package base.thread;
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("start MyThread!");
}
}
2,实现java.long.Runnable接口
package base.thread;
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("start MyRunnable!");
}
}
Test类:
package base.thread;
public class Test2 {
public static void main(String[] args) {
//继承Thread类
Thread thread1 = new MyThread();
thread1.start();
//实现Runable接口
Thread thread2 = new Thread(new MyRunnable());
thread2.start();
//java8的lambda
System.out.println("start");
Thread thread3 = new Thread(() -> {
System.out.println("start java8的lambda");
});
//如果调用run的话就相当于在原有的线程内调用了一个对象的方法只有调用start的时候才是创建新线程
// thread3.run();
thread3.setPriority(4); 1~10, 默认值
thread3.start();
System.out.println("end");
}
}
3,Thread和runnable实现的线程区别:
实现Runnable接口比继承Thread类所具有的优势:
1)适合多个相同的程序代码的线程去处理同一个资源
2)可以避免java中的单继承的限制
3)增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
四 线程同步
多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized
同步;同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;注意加锁对象必须是同一个实例;对JVM定义的单个原子操作不需要同步。
1,synchronized加锁this
上面的代码很简单,两个线程同时对一个int
变量进行操作,一个加10000次,一个减10000次,最后结果应该是0,但是,每次运行,结果实际上都是不一样的。这是因为对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。
//例如,对于语句:
n = n + 1;
//看上去是一行语句,实际上对应了3条指令:
ILOAD
IADD
ISTORE
/**
线程1和2都在ILOAD这一步读取n的值为100,然后开始各自执行IADD和ISTORE,导致
最后一个线程执行ISTORE的时候,会覆盖上一个计算的结果。
**/
我们假设n
的值是100
,如果两个线程同时执行n = n + 1
,得到的结果很可能不是102
,而是101
,原因在于:
如果线程1在执行ILOAD
后被操作系统中断,此刻如果线程2被调度执行,它执行ILOAD
后获取的值仍然是100
,最终结果被两个线程的ISTORE
写入后变成了101
,而不是期待的102
。
这说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待:
通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。
可见,保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized
关键字对一个对象进行加锁:
synchronized(lock) {
n = n + 1;
}
synchronized
保证了代码块在任意时刻最多只有一个线程能执行。我们把上面的代码用synchronized
改写如下:
注意到代码:
synchronized(Counter.lock) { // 获取锁
...
} // 释放锁
它表示用Counter.lock
实例作为锁,两个线程在执行各自的synchronized(Counter.lock) { … }
代码块时,必须先获得锁,才能进入代码块进行。执行结束后,在synchronized
语句块结束会自动释放锁。这样一来,对Counter.count
变量进行读写就不可能同时进行。上述代码无论运行多少次,最终结果都是0。
使用synchronized
解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized
代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized
会降低程序的执行效率。
我们来概括一下如何使用synchronized
:
- 找出修改共享变量的线程代码块;
- 选择一个共享实例作为锁;
- 使用
synchronized(lockObject) { … }
。在使用synchronized
的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized
结束处正确释放锁:
我们知道Java程序依靠synchronized
对线程进行同步,使用synchronized
的时候,锁住的是哪个对象非常重要。让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把synchronized
逻辑封装起来。例如,我们编写一个计数器如下:
public class Counter {
private int count = 0;
public void add(int n) {
synchronized(this) {
count += n;
}
}
public void dec(int n) {
synchronized(this) {
count += n;
}
}
//dec的写法等价于dec2。表示整个方法都必须用this实例加锁。
public synchronized void dec2(int n) {
count += n;
}
public int get() {
return count;
}
}
2,synchronized加锁Class
对于static
方法,是没有this
实例的,因为static
方法是针对类而不是实例。但是我们注意到任何一个类都有一个由JVM自动创建的Class
实例,因此,对static
方法添加synchronized
,锁住的是该类的class
实例。上述计数器改编如下:
public class Counter {
private int count = 0;
public void add(int n) {
synchronized(this) {
count += n;
}
}
public void dec(int n) {
synchronized(this) {
count += n;
}
}
//dec的写法等价于dec2。表示整个方法都必须用this实例加锁。
public synchronized void dec2(int n) {
count += n;
}
public int get() {
return count;
}
//锁住Class
public static void test(int n) {
synchronized(Counter.class) {
...
}
}
//test等价于test1
public synchronized static void test2(int n) {
...
}
}
3,不需要synchronized的操作
JVM规范定义了几种原子操作:
- 基本类型(
long
和double
除外)赋值,例如:int n = m
; - 引用类型赋值,例如:
List<String> list = anotherList
。
long
和double
是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把long
和double
的赋值作为原子操作实现的。
四 线程锁
1,可重入锁:
Java的线程锁是可重入的锁。什么是可重入的锁?我们还是来看例子:
public class Counter {
private int count = 0;
public synchronized void add(int n) {
if (n < 0) {
dec(-n);
} else {
count += n;
}
}
public synchronized void dec(int n) {
count += n;
}
}
观察synchronized
修饰的add()
方法,一旦线程执行到add()
方法内部,说明它已经获取了当前实例的this
锁。如果传入的n < 0
,将在add()
方法内部调用dec()
方法。由于dec()
方法也需要获取this
锁,现在问题来了:
对同一个线程,能否在获取到锁以后继续获取同一个锁?
答案是肯定的。JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized
块,记录-1,减到0的时候,才会真正释放锁。
2,死锁
一个线程可以获取一个锁后,再继续获取另一个锁。例如:
public void add(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value += m;
synchronized(lockB) { // 获得lockB的锁
this.another += m;
} // 释放lockB的锁
} // 释放lockA的锁
}
public void dec(int m) {
synchronized(lockB) { // 获得lockB的锁
this.another -= m;
synchronized(lockA) { // 获得lockA的锁
this.value -= m;
} // 释放lockA的锁
} // 释放lockB的锁
}
在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁。对于上述代码,线程1和线程2如果分别执行add()
和dec()
方法时:
- 线程1