Java并发编程(上)
什么是进程?
进程就是一段程序的执行过程。比如java程序
什么是线程?
线程是运行在进程中细分的任务执行过程。比如一个Java程序正在用一个线程做计算任务。
线程的几种状态
- New(新建)
- Runnable(可运行)
- Blocked(阻塞)
- Waiting(等待)
- Timed Waiting(计时等待)
- Terminated(终止)
New(新建)
当创建一个线程并且还没有调用start方法时,这个线程目前就是等待状态。
Thread t1 = new Thread(() -> System.out.println());
这里使用一个实现了Runnable
接口的匿名类对象创建了一个线程对象。此时这个线程的状态为New
Runnable(可运行)
Thread t1 = new Thread(() -> System.out.println());
t1.start();
System.out.println(t1.getState());
输出:RUNNABLE
只要调用了start
方法,线程就变为可运行状态。并且会由操作系统负责安排其运行时间。
Blocked(阻塞)
当有多个线程共同访问一个资源(对象),其中一个线程占用了当前对象的内部对象锁,其他的线程就需要等待对象锁被释放。这个等待的过程叫阻塞。
Thread t1 = new Thread(() -> {
while (true)
new AA().print();
});
Thread t2 = new Thread(()->{
new AA().print();
});
Thread t3 = new Thread(() -> System.out.println(t2.getState()));
t1.start();
t2.start();
t3.start();
输出:BLOCKED
其中AA
对象的print()
方法是被synchronized
关键字标记的,因为第一个线程一直占用AA
对象的内部锁,所以第二个线程就会一直阻塞,直到锁被释放。
Waiting(等待)
指其他线程等待一个线程通知调度器其他线程可以获取锁的过程
Thread t2 = new Thread(()->{
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
上面的线程会等待t1
线程释放锁之后再执行,在等待的过程中,t2
的状态为WAITING
Timed Waiting(计时等待)
Thread t2 = new Thread(()->{
try {
t1.join(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread t3 = new Thread(() -> System.out.println(t2.getState()));
上面的程序中,t1.join(2)
表示等待t1
线程2毫秒
,之后开始执行t2
的代码。
Terminated(终止)
当前线程中的run
方法执行完毕,线程会自然终止,或者如果程序出现异常并且没有捕获,线程会意外终止
线程属性
中断线程
除了stop
方法外,目前没有办法强制中断线程,但是stop
方法已经废弃了。现在只能在阻塞状态的线程对象使用interrupt()
方法为线程设置一个中断状态
,表示请求中断这个线程,线程将会被InterruptedException
异常中断。这个方法对于正在运行中的线程没有任何作用。
Thread t2 = new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t1.start();
t2.start();
t2.interrupt();
上面的程序将会被InterruptedException
异常终止。需要注意的是,如果先设置了中断状态,然后又调用了sleep
方法,程序会清除中断状态并抛出InterruptedException
异常。
有两个检查线程中断状态的方法interrupted和isInterrupted,他们的区别在于前者会改变中断状态为false,后者不会。
守护线程
为其他线程服务的线程被称为守护线程,通过setDaemon(true)
指定一个守护线程。当只剩下守护线程时,虚拟机会退出。
未捕获异常的处理器
run方法无法抛出检查型异常,但是在线程终止之前,异常会传递到未捕获异常的处理器。默认的处理器是实现了Thread.UncaughtExceptionHandler
接口的类ThreadGroup
,它有一个方法叫做uncaughtException(Thread t,Throwable e)
。其中Throwable存储了栈轨迹,可以输出错误流。
当然也可以自己创建一个处理器,通过Thread的静态方法setDefaultUncaghtExceptionHandler
为所有线程安装一个默认处理器,或使用setUncaghtExceptionHandler
为一个线程安装处理器。但是前提是这个处理器必须是实现了Thread.UncaughtExceptionHandler
接口的实现类。
线程优先级
t2.setPriority(1);
可以通过setPriority
方法为当前线程指定优先级,线程调度器会优先调用优先级高的线程执行。1
表示最小优先级,10
表示最大优先级,默认优先级是5
。
现在不推荐使用线程优先级了,因为操作系统的优先级与Java虚拟机的线程优先级并不是相同的。
线程同步
public class JucTest2 {
private static Integer count = 0;
public static void main(String[] args) throws InterruptedException {
var num = 100;
while (num>0){
new Thread(() -> {
for (int i = 0; i < 10; i++) {
count +=1;
}
}).start();
num--;
}
Thread.sleep(2000);
System.out.println(count);
}
}
上面的代码运行之后理想的结果应该是1000
,但是却输出了982
,这就是因为线程不同步导致的。当一个线程正在修改count
的值,这个操作并不是原子操作。count+=1
这句代码在被翻译为虚拟机字节码后实际上是由多行字节码指令组成,在一个线程执行这些指令到一半的时候,有可能被其他线程抢占运行权导致线程中断,待抢占了运行权的线程执行完毕后再继续运行完剩下的代码。就很容易出错。
比如线程1
修改count到一半,线程2
抢占了运行权,修改了count;然后线程1
继续运行完剩下的任务将count修改完成,注意这个时候,线程1
的修改结果就会覆盖线程2
的修改结果。这就是因为线程访问数据次序问题导致的错误,一个线程还没等另一个线程执行完就抢占了运行权,我们将这称之为静态条件。
锁
针对上述因并发导致的对象状态被破坏问题,有两种解决方案:1、使用ReentrantLock
类,2、使用synchronized
关键字
ReentrantLock(重入锁)
new Thread(() -> {
rlock.lock();
try {
for (int i = 0; i < 10; i++) {
count +=1;
}
}finally {
rlock.unlock();
}
}).start();
使用ReentrantLock
对象的lock
方法表示此时只有锁定了锁对象的这个线程能够执行临界区的代码,其他所有线程都会阻塞,只有等当前线程调用ReentrantLock
对象的unlock
方法后才能试着抢占锁。
unlock方法必须要放在finally中,否则如果代码抛出异常,其他线程将永远阻塞
为什么叫重入锁呢?
public void ceshi() throws InterruptedException {
var num = 100;
while (num>0){
new Thread(() -> {
rlock.lock();
rlock.lock();
print();
try {
for (int i = 0; i < 10; i++) {
count +=1;
}
}finally {
rlock.unlock();
rlock.unlock();
}
}).start();
num--;
}
Thread.sleep(2000);
System.out.println(count);
}
public void print(){
System.out.println(rlock.getHoldCount());
}
示例代码中执行了两次rlock.lock()
,即表示加了两次锁或重入了两次。当程序运行到print()
方法控制台会打印2
,getHoldCount()
可以获得一个锁对象持有的锁的数量。当然,加了几把锁就要释放几把锁,所以finally中rlock.unlock()
也要执行两次。
条件对象
当一个线程执行任务时发现,只有当符合某些条件时才能够继续执行。在这种情况下需要使用到条件对象
private Condition condition = rlock.newCondition();
@Test
public void ceshi() {
for (int i = 0; i < 50; i++) {
new Thread(() -> {
rlock.lock();
try {
count = (int)(Math.random() * 100);
System.out.println(count);
while(count>50){
condition.await();
}
condition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
rlock.unlock();
}
}).start();
}
}
上面的代码中,如果count
大于50,就会调用await()
使当前线程暂停并放弃锁,当其他的线程修改了count
并调用signalAll()
通知等待的线程:可以再检查下条件。这时等待的线程从暂停的地方继续执行,循环判断条件是否成立,如果成立继续使当前线程暂停并放弃锁,否则往下执行。
synchronized(内部对象锁)
实际上每个对象都有一个内部锁,并且与重入锁不同的是内部对象锁只有一个关联条件。
@Test
public void ceshi() {
for (int i = 0; i < 50; i++) {
new Thread(() -> {
synchTest();
}).start();
}
}
public synchronized void synchTest(){
try {
count = (int)(Math.random() * 100);
System.out.println(count);
while(count>50){
wait();
}
notifyAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
除了不需要创建对象,只需要在方法中添加一个关键字外,synchronized
的用法没有太大的区别。wait()
和notifyAll()
方法其实对应重入锁获取的Condition
对象的await()
和signalAll()
。不同的是synchronized
无法重复获得已经获得的锁并且只有一个关联条件,只能调用一次wait()
。其实就是wait()
只能将一个线程添加到等待集,而await()
可以添加多个线程到等待集。
同步代码块
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
try {
synchronized (obj){
System.out.println(Thread.currentThread().getName());
Thread.sleep(3000);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
}
synchronized(Object){}
创建了一个同步的代码块,其效果与写在方法生命上的Synchronized关键字一样。之所以传入一个对象,是为了让同步代码块拿到一个对象锁。
监视器的概念(仅仅是概念)
如果一个方法用synchronized
声明,它就是一个监视器方法。内部锁是存在于对象内部的数据结构,监视器是和对象的锁关联的,它可以操控线程获得锁和释放锁。
转载自:https://juejin.cn/post/7233426392095490105