线程
# 线程状态
- New:新创建的线程,尚未执行;
- Runnable:运行中的线程,正在执行run()方法的Java代码;
- Blocked:运行中的线程,因为某些操作被阻塞而挂起;
- Waiting:运行中的线程,因为某些操作在等待中;
- Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
- Terminated:线程已终止,因为run()方法执行完毕。
示例
join()
等待该线程结束,然后才继续往下执行自身线程
@Test
public void case1() throws Exception {
Thread t = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
System.out.println("start() 前,Thread T 状态:"+t.getState());
t.start();
System.out.println("start() 后,Thread T 线程状态:"+t.getState());
//join()等待该线程结束,然后才继续往下执行自身线程
t.join();
System.out.println("join() 后,Thread T 状态:"+t.getState());
System.out.println("程序结束");
}
log
start() 前,Thread T 状态:NEW
start() 后,Thread T 线程状态:RUNNABLE
join() 后,Thread T 状态:TERMINATED
如果上述示例不加 join
, Thread Main
将不会等待 Thread t
执行,从而导致程序直接退出。
# 中断线程
interrupt();
需要注意的是,他并不会直接中断线程,需要线程内部监听 isInterrupted()
返回值
示例
Thread t = new Thread(() -> {
System.out.println("Thread t:"+Thread.currentThread().getState());;
//监听是否中断
while (!Thread.currentThread().isInterrupted()){
System.out.println("正在运行中。。。"+ false);
}
});
t.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//线程中断
t.interrupt();
try {
//等待检查执行完毕
t.join();
} catch (InterruptedException e) {
System.out.println("程序已经 interrupt()");
}
System.out.println("程序结束");
# 守护线程
java程序的所有线程都是由jvm创建的,也就是说,只要有任意一个jvm创建的线程没有退出,那么jvm就不会退出。
所以线程需要有负责人对他进行关闭,但是一些线程,例如定时任务通常是无线循环,且无人它负责的关闭。
这类线程需要随着系统的退出而关闭,对于这类线程通常会用到守护线程。
需要注意的是:
守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。
示例
Thread thread = new Thread(() -> {
while (true){
}
});
//设置守护线程
thread.setDaemon(true);
thread.start();
System.out.println("程序结束"));
# 线程同步
多线程的模式下,对同一个模型进行操作,结果往往不尽如意,例如一以下示例:
示例
static int n = 0;
public static void main(String[] args) {
Thread a = new Thread(()->{
for (int i = 0; i < 1000; i++) {
n = n++;
}
});
Thread b = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
n = n--;
}
});
b.start();
a.start();
try {
a.join();
b.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(n);
}
上述程序中: A,B两个线程分别对n
进行了1000次加,减操作。按照预期最终结果应该为:0。
实际上并不是这样,n = n++;
这行代码实际上分为3个步骤
- ILOAD
- IADD
- ISTORE
例如: n=10 ,n=n++; 对应的步骤是:
- ILOAD -> n=10
- IADD -> +1操作
- ISTORE -> n=11
但是有这样一种场景: A线程对N进行+1操作, 在没有到 ISTORE
阶段时, B线程对N进行-1操作,ILOAD
得到的是为变更结果
代入上面推断
- A -> 10 +1 -> 11
- B -> 10 -1 -> 9
- 最终结果:n=9
所以上述程序有时候的得不到预期的结果:0
为了解决上述问题,我们需要保证,在进行一次变更操作的时候,别的线程不能干扰它,也就是阻断别的线程。
可通过 synchronized
关键字对一个对象进行加锁,保证了代码块在任意时刻最多只有一个线程能执行。
改动代码如下
示例
static int n = 0;
//锁对象
static final Object LOCK = new Object();
public static void main(String[] args) {
Thread a = new Thread(()->{
for (int i = 0; i < 1000; i++) {
synchronized (LOCK){
n = n++;
}
}
});
Thread b = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
synchronized (LOCK){
n = n--;
}
}
});
b.start();
a.start();
try {
a.join();
b.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(n);
}
synchronized
保证了代码块中数据的原子性,同样JVM规范定义了几种原子操作 :
- 基本类型赋值 (long和double除外)
- 例如:
int n = m
- 例如:
- 引用类型赋值:
- 例如:
List<String> list = anotherList
- 例如:
- 不可变对象无需同步
同样使用 synchronized
需要注意粒度,对于一个锁对象,应该应用于同类事务。不同类事务的操作,应该定义与之对应的锁对象。
例如:分别统计男女性别数量
int boyCount=0;
int girlCount=0
static final Object LOCK = new Object();
addBoy(){
synchronized (LOCK){
boyCount= boyCount++;
}
}
addGirl(){
synchronized (LOCK){
girlCount= girlCount++;
}
}
都是进行+1操作。如果同一把锁进行保证同步,那么将会损失一部分性能。因为这两个统计是可以分开进行的,这类场景就可以男女分别设置一把锁。
int boyCount=0;
int girlCount=0
static final Object BOY_LOCK = new Object();
static final Object GIRL_LOCK = new Object();
void addBoy(){
synchronized (BOY_LOCK){
boyCount= boyCount++;
}
}
void addGirl(){
synchronized (GIRL_LOCK){
girlCount= girlCount++;
}
}
# 同步方法
在使用synchronized
对线程进行同步,使用synchronized的时候,锁住的是哪个对象非常重要。
但是在方法内定于过多的锁对象,会使代码变得凌乱且不易于维护。
对于大部分方法而言,我们想要的通常是:使用一个实例对象调用方法时,不被别的线程调用同一个实例对象所影响,因此我们需要锁住的其实是这个实例本身。
锁住实例本身有两种方式,他们效果相同:
int boyCount=0;
void addBoy(){
synchronized (this){
boyCount= boyCount++;
}
}
int boyCount=0;
synchronized void addBoy(){
(this){
boyCount= boyCount++;
}
}
因此,用 synchronized
修饰的方法就是同步方法,它表示整个方法都必须用this实例
加锁。
当调用的是静态方法时:
public class Test {
public synchronized void test(int n) {
}
}
由于静态方法没有 this
对象,它锁住的是当前的Class实例
,由jvm创建 ,因此静态方法的同步也可以这样写
public class Test {
public static void test(int n) {
synchronized(Test.class) {
}
}
}
# 可重入锁
synchronized
代码块所加的锁为可重入锁,也就是说,在获取到当前对象的锁后,可以继续获取同一个锁。
如下
public class Test {
public synchronized void test(int n) { //得到锁
test2(n);
}//释放锁
public synchronized void test2(int n) {//得到锁
}//释放锁
}
可重入锁在获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized
块,记录-1,减到0的时候,才会真正释放锁。
# 死锁
死锁产生的条件是多线程各自持有不同的锁,并互相试图获取对方已持有的锁,导致无限等待。
由于 synchronized
为可重入锁,那么在如下程序
public class Test {
static final Object LOCK_1 = new Object();
static final Object LOCK_2 = new Object();
public void test1(int n) { //得到锁
System.out.println("得到方法test1的锁");
synchronized (LOCK_1){
test2(n);
synchronized (LOCK_2){
System.out.println("test1");
}
}
}//释放锁
public void test2(int n) {//得到锁
System.out.println("得到方法test1的锁");
synchronized (LOCK_2){
test1(n);
synchronized (LOCK_1){
System.out.println("test2");
}
}
}//释放锁
}
此时如果有两个线程A,B。
A 调用了test1
拿到了锁 LOCK_1
,准备获取LOCK_2
。
于此同时,B 调用了test2
拿到了锁 LOCK_2
,准备获取LOCK_1
。
这时候 test2
无法获取 LOCK_1
,test1
无法获取 LOCK_2
, 双方都在等待对方释放锁。导致形成死锁。
所以多线程获取锁的顺序要一致,以避免出现死锁。