《实战高并发程序设计》读书笔记-线程基本知识补充

volatile

  Java内存模型都是围绕着原子性、有序性和可见性展开的,为了在适当的场合,确保线程间的有序性、可见性和原子性。Java使用了一些特殊的操作或者关键字来申明、告诉虚拟机,在这个地方,要尤其注意,不能随意变动优化目标指令。关键字volatile就是其中之一,用于保持线程之间的\color{red}{可见性}

  但是同时应该注意的是,volatile并不能当锁用,他无法保证共享变量操作的原子性。比如下面的例子,通过volatile是无法保证i++的原子性操作的:

01 static volatile int i=0;
02 public static class PlusTask implements Runnable{
03     @Override
04     public void run() {
05         for(int k=0;k<10000;k++)
06             i++;
07     }
08 }
09
10 public static void main(String[] args) throws InterruptedException {
11     Thread[] threads=new Thread[10];
12     for(int i=0;i<10;i++){
13         threads[i]=new Thread(new PlusTask());
14         threads[i].start();
15     }
16     for(int i=0;i<10;i++){
17         threads[i].join();
18     }
19
20     System.out.println(i);
21 }

  执行上述代码,如果第6行i++是原子性的,那么最终的值应该是100000(10个线程各累加10000次)。但实际上,上述代码的输出总是会小于100000。

重点

\color{red}{只能保证线程之间的可见性,和程序的有序性,不能保证共享变量操作的原子性}。

线程组

可以通过线程组对不同的线程分批管理

比如:

01 public class ThreadGroupName implements Runnable {
02     public static void main(String[] args) {
03         ThreadGroup tg = new ThreadGroup("PrintGroup");
04         Thread t1 = new Thread(tg, new ThreadGroupName(), "T1");
05         Thread t2 = new Thread(tg, new ThreadGroupName(), "T2");
06         t1.start();
07         t2.start();
08         System.out.println(tg.activeCount());
09         tg.list();
10     }
11
12     @Override
13     public void run() {
14         String groupAndName=Thread.currentThread().getThreadGroup().getName()
15                 + "-" + Thread.currentThread().getName();
16         while (true) {
17             System.out.println("I am " + groupAndName);
18             try {
19                 Thread.sleep(3000);
20             } catch (InterruptedException e) {
21                 e.printStackTrace();
22             }
23         }
24     }
25 }

通过\color{red}{ThreadGroup}创建一个名为“PrintGroup”的线程组,并将T1和T2两个线程加入这个组中。第8、9两行,展示了线程组的两个重要的功能,activeCount()可以获得活动线程的总数,但由于线程是动态的,因此这个值只是一个估计值,无法确定精确,list()方法可以打印这个线程组中所有的线程信息,对调试有一定帮助。代码中第4、5两行创建了两个线程,使用Thread的构造函数,指定线程所属的线程组,将线程和线程组关联起来。
线程组还有一个值得注意的方法stop(),它会停止线程组中所有的线程。这看起来是一个很方便的功能,但是它会遇到和Thread.stop()相同的问题(终止业务逻辑没有处理完的线程),因此使用时也需要格外谨慎。

重点

通过ThreadGroup为线程分组,并且为之按照业务需要命名,让线程的名字更有意义,慎用线程组的stop(),这会导致破坏线程业务逻辑的原子性, 线程组的activeCount()可以获得活动线程的总数,并且可以通过list()方法可以打印这个线程组中所有的线程信息,对调试有一定帮助

守护线程(Daemon)

  守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程就可以理解为守护线程。\color{red}{与之相对应的是用户线程,用户线程可以认为是系统的工作线程},它会完成这个程序应该要完成的业务操作。\color{red}{如果用户线程全部结束,这也意味着这个程序实际上无事可做了}。守护线程要守护的对象已经不存在了,那么整个应用程序就自然应该结束。因此,\color{red}{当一个Java应用内,只有守护线程时,Java虚拟机就会自然退出}

01 public class DaemonDemo {
02     public static class DaemonT extends Thread{
03         public void run(){
04             while(true){
05                 System.out.println("I am alive");
06                 try {
07                     Thread.sleep(1000);
08                 } catch (InterruptedException e) {
09                     e.printStackTrace();
10                 }
11             }
12         }
13     }
14     public static void main(String[] args) throws InterruptedException {
15         Thread t=new DaemonT();
16         t.setDaemon(true);
17         t.start();
18
19         Thread.sleep(2000);
20     }
21 }

  上述代码第16行,将线程t设置为守护线程。这里注意,\color{red}{设置守护线程必须在线程start()之前设置},否则你会得到一个类似以下的异常,告诉你守护线程设置失败。但是你的程序和线程依然可以正常执行。只是被当做用户线程而已。因此,如果不小心忽略了下面的异常信息,你就很可能察觉不到这个错误。

Exception in thread "main" java.lang.IllegalThreadStateException
    at java.lang.Thread.setDaemon(Thread.java:1367)
    at geym.conc.ch2.daemon.DaemonDemo.main(DaemonDemo.java:20)

  在这个例子中,由于t被设置为守护线程,系统中只有主线程main为用户线程,因此在main线程休眠2秒后退出时,整个程序也随之结束。但如果不把线程t设置为守护线程,main线程结束后,t线程还会不停地打印,永远不会结束。

重点

设置守护线程必须在线程start()之前设置,否则会得到以下异常,导致守护线程失效,但是不影响主线程执行。

java.lang.IllegalThreadStateException

线程优先级

在Java中,使用1到10表示线程优先级。一般可以使用内置的三个静态标量表示:

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 1

数字越大则优先级越高,但有效范围在1到10之间。下面的代码展示了优先级的作用。高优先级的线程倾向于更快地完成。

01 public class PriorityDemo {
02     public static class HightPriority extends Thread{
03         static int count=0;
04         public void run(){
05             while(true){
06                 synchronized(PriorityDemo.class){
07                     count++;
08                     if(count>10000000){
09                         System.out.println("HightPriority is complete");
10                         break;
11                     }
12                 }
13             }
14         }
15     }
16     public static class LowPriority extends Thread{
17         static int count=0;
18         public void run(){
19             while(true){
20                 synchronized(PriorityDemo.class){
21                     count++;
22                     if(count>10000000){
23                         System.out.println("LowPriority is complete");
24                         break;
25                     }
26                 }
27             }
28         }
29     }
30
31     public static void main(String[] args) throws InterruptedException {
32         Thread high=new HightPriority();
33         LowPriority low=new LowPriority();
34         high.setPriority(Thread.MAX_PRIORITY);
35         low.setPriority(Thread.MIN_PRIORITY);
36         low.start();
37         high.start();
38     }
39 }

  上述代码定义两个线程,分别为HightPriority设置为高优先级,LowPriority为低优先级。让它们完成相同的工作,也就是把count从0加到10000000。完成后,打印信息给一个提示,这样我们就知道谁先完成工作了。这里要注意,在对count累加前,我们使用synchronized产生了一次资源竞争。目的是使得优先级的差异表现得更为明显。
  大家可以尝试执行上述代码,可以看到,高优先级的线程在大部分情况下,都会首先完成任务(就这段代码而言,试运行多次,HightPriority总是比LowPriority快,但这不能保证在所有情况下,一定都是这样)。

重点

可以通过通过setPriority()设置线程的优先级,从0到10,优先级越高越容易抢占资源,但这不是绝对的,只是说理论上抢占共享资源的几率更高。

synchronized

  Java 内置的管程方案(synchronized)使用简单,synchronized 关键字修饰的代码块,在编译期会自动生成相关加锁和解锁的代码,但是仅支持一个条件变量;而 Java SDK 并发包实现的管程支持多个条件变量,不过并发包里的锁,需要开发人员自己进行加锁和解锁操作。

  并发编程里两大核心问题——互斥和同步,都可以由管程来帮你解决。学好管程,理论上所有的并发问题你都可以解决,并且很多并发工具类底层都是管程实现的,所以学好管程,就是相当于掌握了一把并发编程的万能钥匙。

  关键字synchronized的作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性

synchronized的多种用法

  • 指定加锁对象:
    • 对给定\color{red}{对象加锁},进入同步代码前要获得给定对象的锁。
  • 直接作用于实例方法:
    • 相当于对当前\color{red}{实例加锁},进入同步代码前要获得当前实例的锁。
  • 直接作用于静态方法:
    • 相当于对当前\color{red}{类加锁},进入同步代码前要获得当前类的锁。

以上是原书给的分类,但是我感觉可以按照锁对象这个维度来看synchronized的用法:

锁对象

锁实例对象
1.修饰普通方法,锁的是this
image.png

此时锁对象是this,该类的实例对象。

如果实例对象不是一个,那么保证不了共享资源的安全。

2.synchronized(this)代码块
image.png
类锁,锁的是内存中唯一存在的class对象
1.修饰静态方法
image.png
2.synchronized(*.class)代码块
image.png

synchronized的配套使用

image.png
image.png

synchronized的使用例子

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    @Override
    public void run() {
        for(int j=0;j<10000000;j++){
            //类锁
            synchronized(instance){
                i++;
            }
        }
    }

当然,上述代码也可以写成如下形式,两者是等价的:

01 public class AccountingSync2 implements Runnable{
02     static AccountingSync2 instance=new AccountingSync2();
03     static int i=0;
04     public synchronized void increase(){
05         i++;
06     }
07     @Override
08     public void run() {
09         for(int j=0;j<10000000;j++){
10             increase();
11         }
12     }
13     public static void main(String[] args) throws InterruptedException {
14         Thread t1=new Thread(instance);//锁的是当前实例,但是两个线程是同一个锁对象
15         Thread t2=new Thread(instance);
16         t1.start();t2.start();
17         t1.join();t2.join();
18         System.out.println(i);
19     }
20 }

上述代码中,synchronized关键字作用于一个实例方法。这就是说在进入increase()方法前,线程必须获得当前对象实例的锁。在本例中就是instance对象。在这里,我不厌其烦地再次给出main函数的实现,是希望强调第14、15行代码,也就是Thread的创建方式。这里使用Runnable接口创建两个线程,并且这两个线程都指向同一个Runnable接口实例(instance对象),这样才能保证两个线程在工作时,能够关注到同一个对象锁上去,从而保证线程安全。
一种错误的同步方式如下

01 public class AccountingSyncBad implements Runnable{
02     static int i=0;
03     public synchronized void increase(){
04         i++;
05     }
06     @Override
07     public void run() {
08         for(int j=0;j<10000000;j++){
09             increase();
10         }
11     }
12     public static void main(String[] args) throws InterruptedException {
13         Thread t1=new Thread(new AccountingSyncBad());
14         Thread t2=new Thread(new AccountingSyncBad());
15         t1.start();t2.start();
16         t1.join();t2.join();
17         System.out.println(i);
18     }
19 }

  上述代码就犯了一个严重的错误。虽然在第3行的increase()方法中,申明这是一个同步方法。但是当锁对象是当前实例,两个线程分别分配了不同实例,导致锁对象不同,不是一把锁,线程安全无法保证。
但我们只要简单地修改上述代码,就能使其正确执行。那就是使用synchronized的第三种用法,将其作用于静态方法。将increase()方法修改如下:

public static synchronized void increase(){
    i++;

  除了用于线程同步、确保线程安全外,synchronized还可以保证线程间的可见性和有序性。从可见性的角度上讲,synchronized可以完全替代volatile的功能,只是使用上没有那么方便。就有序性而言,由于synchronized限制每次只有一个线程可以访问同步块,因此,无论同步块内的代码如何被乱序执行,只要保证串行语义一致,那么执行结果总是一样的。而其他访问线程,又必须在获得锁后方能进入代码块读取数据,因此,它们看到的最终结果并不取决于代码的执行过程,从而有序性问题自然得到了解决(换言之,被synchronized限制的多个线程是串行执行的)。

notice:

什么是管程

所谓管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。翻译为 Java 领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。

Java 采用的是管程技术,synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法都是管程的组成部分。而管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程。但是管程更容易使用,所以 Java 选择了管程。

管程,对应的英文是 Monitor,而不是直译为“监视器”。

如果对管程这种技术思想有兴趣,可以继续看我的关于这块的笔记。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容