并发编程总结

可重入内置锁

同一个线程在调用本类中其他synchronized方法/块或父类中的synchronized方法/块时,都不会阻碍该线程地执行,因为互斥锁时可重入的。

中断线程

使用interrupt()中断线程

当一个线程运行时,另一个线程可以调用对应的Thread对象的interrupt()方法中断它。该方法只是在目标线程中设置了一个标志位,表示它已经被中断,并立即返回。如果只是单纯的调用interrupt()方法,线程并没有实际中断,会继续执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class SleepInterrupt extends Object implements Runnable{
public void run() {
try {
System.out.println("in run() - about to sleep for 20 seconds");
Thread.sleep(20000);
System.out.println("in run() - woke up");
} catch(InterruptedException e) {
System.out.println("in run() - interrupted while sleeping");
//处理完中断异常后,返回到run()方法人口,
//如果没有return,线程不会实际被中断,它会继续打印下面的信息
return;
}
System.out.println("in run() - leaving normally");
}

public static void main(String[] args) {
SleepInterrupt si = new SleepInterrupt();
Thread t = new Thread(si);
t.start();
//主线程休眠2秒,从而确保刚才启动的线程有机会执行一段时间
try {
Thread.sleep(2000);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("in main() - interrupting other thread");
//中断线程t
t.interrupt();
System.out.println("in main() - leaving");
}
}

运行结果:

主线程启动新线程后,自身休眠2秒钟,允许新线程获得运行时间。新线程打印信息“about to sleep for 20 seconds”后,继而休眠20秒钟,大约2秒钟后,main线程通知新线程中断,那么新线程的20秒的休眠将被打断,从而抛出InterruptException异常,执行跳转到catch块,打印出“interrupted while sleeping”信息,并立即从run()方法返回,然后消亡,而不会打印出catch块后面的“leaving normally”信息。
注意:如果将catch块中的return语句注释掉,则线程在抛出异常后,会继续执行,而不会中断,从而打印出”leaving normally“信息。

待决中断

上面的例子中断是在sleep()方法之后调用的,它会相当友好地终止线程,并抛出InterruptedException异常。如果在调用sleep()方法前被中断,则该中断被称为待决中断,它会在调用sleep()方法时,立即出InterruptedException异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class PendingInterrupt extends Object {
public static void main(String[] args) {
//如果输入了参数,则在mian线程中中断当前线程(亦即main线程)
if (args.length > 0 ) {
Thread.currentThread().interrupt();
}
//获取当前时间
long startTime = System.currentTimeMillis();
try {
Thread.sleep(2000);
System.out.println("was NOT interrupted");
} catch (InterruptedException x) {
System.out.println("was interrupted");
}
//计算中间代码执行的时间
System.out.println("elapsedTime=" + (System.currentTimeMillis() - startTime));
}
}

执行结果如下:

使用isInterrupted()方法判断中断状态

可以在Thread对象上调用isInterrupted()方法来检查任何线程的中断状态。这里需要注意:线程一旦被中断,isInterrupted()方法便会返回true,而一旦sleep()方法抛出异常,它将清空中断标志,此时isInterrupted()方法将返回false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class InterruptCheck extends Object {
public static void main(String[] args) {
Thread t = Thread.currentThread();
System.out.println("Point A: t.isInterrupted()=" + t.isInterrupted());
//待决中断,中断自身
t.interrupt();
System.out.println("Point B: t.isInterrupted()=" + t.isInterrupted());
System.out.println("Point C: t.isInterrupted()=" + t.isInterrupted());

try {
Thread.sleep(2000);
System.out.println("was NOT interrupted");
} catch ( InterruptedException x) {
System.out.println("was interrupted");
}
//抛出异常后,会清除中断标志,这里会返回false
System.out.println("Point D: t.isInterrupted()=" + t.isInterrupted());
}
}

运行结果如下:

使用Thread.interrupted()方法判断中断标志位

可以使用Thread.interrupted()方法来检查当前线程的中断状态(并隐式重置为false),由于该方法是静态方法,因此不能在特定的线程下使用,而只能报告调用它的线程的中断状态,如果线程被中断,而且中断状态尚不清楚,那么,这个方法返回true。与isInterrupted()不同,它将自动重置中断状态为false,第二次调用Thread.interrupted()方法,总是返回false,除非中断了线程。

1
2
3
4
5
6
7
8
9
10
11
public class InterruptReset extends Object {
public static void main(String[] args) {
System.out.println(
"Point X: Thread.interrupted()=" + Thread.interrupted());
Thread.currentThread().interrupt();
System.out.println(
"Point Y: Thread.interrupted()=" + Thread.interrupted());
System.out.println(
"Point Z: Thread.interrupted()=" + Thread.interrupted());
}
}

运行结果如下:

补充:

  • join()方法用线程对象调用,如果一个线程A中调用另一个线程B的join方法,线程A会等待线程B执行完毕后再执行。
  • yield方法直接使用Thread类调用,yield让出CPU执行权限,再重新竞争CPU的执行权,如果没有比它优先级高的线程,则依旧不会有变化。

线程挂起、恢复与终止的方法

Java并发编程(3):线程挂起、恢复与终止的正确方法(含代码)

守护线程与线程阻塞

  1. 守护线程
    注意:

    1. setDaemon(true)必须在调用线程的start()方法之前设置,否则会跑出IllegalThreadStateException异常。
    2. 在守护线程中产生的新线程也是守护线程。
    3. 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。
  2. 线程阻塞
    线程阻塞的情况

注意:不是所有的阻塞都是可中断的,以上阻塞状态的前两种可中断,后面两张不可以中断。

volatile变量修饰符

Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才将私有拷贝与共享内存中的原始值进行比较。这样当多个线程同时与某个对象交互时,就必须注意到要让线程及时的得到共享成员变量的变化。而volatile关键字就是提示JVM:对于这个成员变量,不能保存它的私有拷贝,而应直接与共享成员变量交互。

volatile是一种稍弱的同步机制,在访问volatile变量时不会执行加锁操作,也就不会执行线程阻塞,因此volatilei变量是一种比synchronized关键字更轻量级的同步机制。

使用建议:在两个或者更多的线程需要访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,没必要使用volatile。
由于使用volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。

有一个案例分析

Runnable和Thread实现多线程的区别

Java实现多线程的方法:继承Thread类、实现Runnable接口、实现Callable接口。在程序开发中,一般用Runnable接口为主,优势如下:

Runnable的优势.png

以下是两个程序的对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyThread extends Thread {
private int ticket = 5;
public void run() {
for (int i = 0; i < 10; i++) {
if (ticket > 0) {
System.out.println("ticket = " + ticket--);
}
}
}
}

public class ThreadDemo{
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
new MyThread().start();
}
}

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyThread implements Runnable {
    private int ticket = 5;
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (ticket > 0) {
                System.out.println("ticket = " + ticket--);
            }
        }
    }
}
 
public class RunnableDemo {
    public static void main(String[] args) {
        MyThread my = new MyThread();
        new Thread(my).start();
        new Thread(my).start();
        new Thread(my).start();
    }
}

输出结果如下:

补充:

  1. 在第二种方法中,输出无法预测,因为ticket-不是原子操作
  2. 在第一种方法中,我们new了3个Thread对象,即三个线程分别执行三个对象中的代码,因此便是三个线程去独立地完成卖票的任务;而在第二种方法中,我们同样也new了3个Thread对象,但只有一个Runnable对象,3个Thread对象共享这个Runnable对象中的代码,因此,便会出现3个线程共同完成卖票任务的结果。如果我们new出3个Runnable对象,作为参数分别传入3个Thread对象中,那么3个线程便会独立执行各自Runnable对象中的代码,即3个线程各自卖5张票。
  3. 在第二种方法中,由于3个Thread对象共同执行一个Runnable对象中的代码,因此可能会造成线程的不安全,比如可能ticket会输出-1(如果我们System.out….语句前加上线程休眠操作,该情况将很有可能出现),这种情况的出现是由于,一个线程在判断ticket为1>0后,还没有来得及减1,另一个线程已经将ticket减1,变为了0,那么接下来之前的线程再将ticket减1,便得到了-1。这就需要加入同步操作(即互斥锁),确保同一时刻只有一个线程在执行每次for循环中的操作。而在第一种方法中,并不需要加入同步操作,因为每个线程执行自己Thread对象中的代码,不存在多个线程共同执行同一个方法的情况。

使用synchronized获取互斥锁

在并发编程中,多线程同时并发访问的资源叫做临界资源,当多个线程同时访问对象并要求操作相同资源时,分割了原子操作就有可能出现数据的不一致或数据不完整的情况,为避免这种情况的发生,我们会采取同步机制,以确保在某一时刻,方法内只允许有一个线程。

采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。

下面是几点说明:

  • 如果同一个方法内同时有两个或更多线程,则每个线程有自己的局部变量拷贝。
  • 类的每个实例都有自己的对象级别锁。当一个线程访问实例对象中的synchronized同步代码块或同步方法时,该线程便获取了该实例的对象级别锁,其他线程这时如果要访问synchronized同步代码块或同步方法,便需要阻塞等待,直到前面的线程从同步代码块或方法中退出,释放掉了该对象级别锁。
  • 访问同一个类的不同实例对象中的同步代码块,不存在阻塞等待获取对象锁的问题,因为它们获取的是各自实例的对象级别锁,相互之间没有影响。
  • 持有一个对象级别锁不会阻止该线程被交换出来,也不会阻塞其他线程访问同一示例对象中的非synchronized代码。当一个线程A持有一个对象级别锁(即进入了synchronized修饰的代码块或方法中)时,线程也有可能被交换出去,此时线程B有可能获取执行该对象中代码的时间,但它只能执行非同步代码(没有用synchronized修饰),当执行到同步代码时,便会被阻塞,此时可能线程规划器又让A线程运行,A线程继续持有对象级别锁,当A线程退出同步代码时(即释放了对象级别锁),如果B线程此时再运行,便会获得该对象级别锁,从而执行synchronized中的代码。
  • 持有对象级别锁的线程会让其他线程阻塞在所有的synchronized代码外。例如,在一个类中有三个synchronized方法a,b,c,当线程A正在执行一个实例对象M中的方法a时,它便获得了该对象级别锁,那么其他的线程在执行同一实例对象(即对象M)中的代码时,便会在所有的synchronized方法处阻塞,即在方法a,b,c处都要被阻塞,等线程A释放掉对象级别锁时,其他的线程才可以去执行方法a,b或者c中的代码,从而获得该对象级别锁。
  • 使用synchronized(obj)同步语句块,可以获取指定对象上的对象级别锁。obj为对象的引用,如果获取了obj对象上的对象级别锁,在并发访问obj对象时时,便会在其synchronized代码处阻塞等待,直到获取到该obj对象的对象级别锁。当obj为this时,便是获取当前对象的对象级别锁。
  • 类级别锁被特定类的所有示例共享,它用于控制对static成员变量以及static方法的并发访问。具体用法与对象级别锁相似。
  • 互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁,如果获得了锁,把锁的计数器加1,相应地,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁便被释放了。由于synchronized同步块对同一个线程是可重入的,因此一个线程可以多次获得同一个对象的互斥锁,同样,要释放相应次数的该互斥锁,才能最终释放掉该锁。

多线程环境下安全使用集合API

Java并发编程(8):多线程环境中安全使用集合API(含代码)

死锁

线程A当前持有互斥所锁lock1,线程B当前持有互斥锁lock2。接下来,当线程A仍然持有lock1时,它试图获取lock2,因为线程B正持有lock2,因此线程A会阻塞等待线程B对lock2的释放。如果此时线程B在持有lock2的时候,也在试图获取lock1,因为线程A正持有lock1,因此线程B会阻塞等待A对lock1的释放。二者都在等待对方所持有锁的释放,而二者却又都没释放自己所持有的锁,这时二者便会一直阻塞下去。这种情形称为死锁。

下面介绍避免死锁的几种常见方法:

  1. 避免一个线程获取多个锁
  2. 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
  3. 尝试使用定时锁,使用lock.tryLock(timeout)来代替使用内部锁机制
  4. 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况

使用wait/notify/notifyAll实现线程间通信

在Java中,可以通过配合调用Object对象的wait()方法和notify()方法或notifyAll()方法来实现线程间的通信。在线程中调用wait()方法,将阻塞等待其他线程的通知(其他线程调用notify()方法或notifyAll()方法),在线程中调用notify()方法或notifyAll()方法,将通知其他线程从wait()方法处返回。

Object是所有类的超类,它有5个方法组成了等待/通知机制的核心:notify()、notifyAll()、wait()、wait(long)和wait(long, int)。在Java中,所有的类都从Object继承而来,因此,所有的类都拥有这些共有方法可供使用。而且,由于他们都被声明为final,因此在子类中不能覆写任何一个方法。

注意:

  • 如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
  • 当有线程调用了对象的notifyAll()方法(唤醒所有wait线程)或notify()方法(只随机唤醒一个wait线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
  • 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了synchronized代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
Author: Toyan
Link: https://toyan.top/java-concurrent-summary/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
支付宝打赏
微信打赏