实战Java高并发程序设计笔记第二章

Java并行程序基础

2.1 线程必知

进程:

  • 资源分配最小单位
  • 进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段

线程:

  • 程序执行的最小单位,轻量级进程
  • 线程没有独立的地址空间,它使用相同的地址空间共享数据
  • 一个进程里面可以有多个线程

为什么要使用线程?

  • 创建一个线程比进程开销小
  • CPU切换一个线程比切换进程花费小
  • 线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据,进程之间的通信需要以通信的方式(IPC)进行;(但多线程程序处理好同步与互斥是个难点)
  • 进程对资源保护要求高,开销大,效率相对较低,线程资源保护要求不高,但开销小,效率高,可频繁切换;
  • 缺点:多进程程序更安全,生命力更强,一个进程死掉不会对另一个进程造成影响(源于有独立的地址空间),多线程程序更不易维护,一个线程死掉,整个进程就死掉了(因为共享地址空间)

线程的生命周期

初始线程:线程的基本操作

新建线程

方法一:继承Thread

Thread t = new Thread(()->System.out.println("Hello,this is created by method1"));//需重载run()方法
t.start();

不要用run()启动线程,注意调用start()和run方法的区别

方法二:实现Runnable接口

实现原理:Thread.run()方法直接调用Runnable实现类对象的run()方法,静态代理

public class CreateThread implements Runnable{
  @Override
  public void run(){
    System.out.println("Hello,this is created by method2");
  }
  public static void main(String[] args){
    Thread t = new Thread(new   CreateThread());
    t.start();
   }
}

方法三:使用Callable和future创建带返回值的线程

创建并启动有返回值的线程的步骤如下:

(1)创建。创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
(2)封装。使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
(3)启动。使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
(4)获取返回值。调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

public class myThread3 implements Callable<String> {

    @Override
    public String call() throws Exception {
        return "hello";
    }

    public static void main(String[] args) {
        FutureTask<String> futureTask = new FutureTask<>(new myThread3());
        Thread t3 = new Thread(futureTask);
        t3.start();
        try {
            System.out.println(futureTask.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


终止线程

  • 方法一:stop(),被废弃不推荐使用

为什么呢?结束线程时,会直接终止线程,会引起数据的一致性问题。可以参考TCP的四次挥手机制中,服务端收到客户端的FIN后,并不会立即关闭自己的服务,而是先发送ACK,将自己这边还未发送的数据继续发送,然后再发送FIN
使用stop()而引起的数据不一致的问题:

public class StopThreadUnsafe{
    public static User u = new User();
    public static class User{
        private int id;
        private String name;
        public User(){
            id=0;
            name="0";
        }
        //省略get,set和toString方法
    }
    public static class ChangeObjectThread extends Thread{
        @Override
        public void run(){
            whie(true){
                synchronized(u){
                    int v = (int)(System.currentTimeMills()/1000);
                    u.setId(v);
                    try{
                        Thread.sleep(100);
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }
                    u.setName(v);
                    Thread.yield();
                }
            }   
        }
    }

    public static class ReadObjectThread extends Thread{
        @Override
        public void run(){
            whie(true){
                synchronized(u){
                    if(u.getId()!=Integer.parseInt(u.getName())){
                        System.out.println(u.toString());
                    }
                    Thread.yield();
                }
            }   
        }
    }
    public static void main(String[] args){
        new ReadObjectThread().start();
        while(true){
            Thread t = new ChangeObjectThread();
            t.start();
            Thread.sleep(150);
            t.stop();
        }
    }
}

如何正确停止线程呢?利用volatile修饰的标志位

public static class ChangeObjectThread extends Thread{
        volatile boolean stopme = false;
        public void stopMe(){
            stopme = true;
        }

        @Override
        public void run(){
            whie(true){
                if(stopme){
                    System.out.println("exit by stop me");
                    break;
                }

                synchronized(u){
                    int v = (int)(System.currentTimeMills()/1000);
                    u.setId(v);
                    try{
                        Thread.sleep(100);
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }
                    u.setName(v);
                    Thread.yield();
                }
            }   
        }
    }


线程中断

  • 一种重要的线程协作机制,并不是使线程立即退出,而是给线程一个通知,告诉目标线程,有人需要你退出了,目标线程收到中断通知后,如何处理自行决定
  • 与中断有关的三个方法:
public void Thread.interrupt()//中断线程
public boolean Thread.isInterrupt()//判断是否被中断
public static boolean Thread.interrupted()//判断是否被中断,并清除当前中断状态
  • 与stopme()手法相似,但中断更厉害,因为wait()和sleep()这类操作也属于中断操作,能被isInterrupt()检测到
public static void main(String[] args){
        Thread t1 = new Thread();
        @Override
        public void run(){
            while(true){
                if(Thread.currentThread().isInterrupted()){
                    System.out.println("Interrupted!");
                    break;
                }
            }

            try{
                Thread.sleep(2000);
            }catch (InterruptedException e){
                System.out.println("Interrupted when sleep");
                Thread.currentThread.interrupt();
            }
            Thread.yield();
        }
        t1.start();
        Thread.sleep(2000);//让当前线程休眠若干时间,抛出一个InterruptedException中断异常
        t1.interrupt();
    }

注意:Thread.sleep()方法由于中断而抛出异常,在捕获异常后,该中断标志位会被清除,因此要再次设置中断标志位,以让下一次循环的开始检测到中断

等待(wait)和通知(notify)

  • 属于Object类的实例方法
public final void wait() throws InterruptedException
public final native void notify() 

wait()和notify()是什么:wait()必须与synchronized搭配使用,当一个对象调用wait()方法,则这个对象所处的线程就会进入object对象的等待队列,在这个等待队列中,可能有多个线程都在等待同一个object对象,当object.notify()调用后,就会从等待队列中(完全)随机选择一个线程将其唤醒
wait()和notify()的工作流程

  • 假设有两个线程T1和T2
  • T1在执行wait()方法前,先通过synchronized获取object对象的监视器,在wait()方法执行后,释放这个监视器

    为什么要释放监视器?
    目的是使得其他等待在object对象上的线程不至于因为T1的休眠而全部无法正常执行

  • T2在notify()调用前,也必须获得object对象的监视器,由于T1已经释放了监视器,所以T2可以顺利获得,T2执行notify()方法唤醒等待线程
  • T1被唤醒后并不会立即去执行后续的代码,而是先尝试重新获取object的监视器,若无法获得,则必须等待这个监视器,当获得这个监视器后,T1开始执行后续代码
    案例:

public class NotifyTest {
    public final  static Object object=new Object();
    public  static class T1 extends  Thread{
        @Override
        public void run() {
            synchronized (object){
                System.out.println(System.currentTimeMillis()+":T1 start");
                System.out.println(System.currentTimeMillis()+": T1 wait for object");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis()+": T1 end");
            }
        }
    }

    public static class T2 extends Thread{
        @Override
        public void run() {
            synchronized (object){
                System.out.println(System.currentTimeMillis()+":T2 start notify object");
                object.notify();
                System.out.println(System.currentTimeMillis()+":T2 end");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread t1=new T1();
        Thread t2=new T2();
        t2.start();
        t1.start();
    }
}

Object,.wait()和Thread.sleep()都能让线程等待若干时间,wait()方法需要被唤醒,在唤醒后hi释放目标对象的锁,而sleep()不会释放任何资源

挂起(suspend)和继续执行(resume)

  • 废弃不推荐使用
  • 如果rusume在suspend前执行,那么被挂起的线程所占用的锁不会被释放,并且其状态为Runnable,影响对当前状态的判断
    例子说明:

如何实现一个可靠的suspend操作呢?利用wait()和notify()

notifyAll():唤醒等待队列中的所有线程,让他们竞争锁

等待线程结束(join)和谦让(yield)

join

  • 应用场景:当一个线程的输入可能依赖于另外一个或多个线程的输出时,当前线程就需要等待依赖线程执行完毕,才能执行
  • jdk提供的两个join()操作

    //无限期等待,当前线程在目标线程执行完毕前一直阻塞,当前线程一直等着目标线程完毕

    public final void join() throws InterruptedException
    

    //当前线程会在规定时间内等待目标线程,一旦超过规定时间,当 前线程就不等了,继续向下执行

    public final synchronized void join(long millis) throws InterruptedException
    
  • 实例程序:
  • join的本质:让调用线程wait()在当前线程对象实例上
      while(isAlive()){
          wait(0); 
       }
    

yield

  public static void yield();

使当前线程让出CPU后继续争夺资源

volatile与Java内存模型(JMM)

volatile

  • volatile无法保证一些符合操作的原子性,例如i++;
  • 可以保证数据的可见性和有序性

分门别类的管理:线程组

驻守后台:守护线程(Daemon)

  • 什么是守护线程?当一个Java应用中只有守护线程时,JVM就会退出
  • 注意:setDaemon需在start之前执行

先干重要的事:线程优先级

  • 线程优先级高的并不一定先执行,只是拥有更多可以执行的机会

线程安全的概念与synchronized

  • 什么是线程安全?

当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类时线程安全的

  • 线程不安全的例子:
  • synchronized介绍

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

  • synchronized的多种用法

指定加锁对象:
直接作用于实例方法:
直接作用域静态方法:
synchronized实现线程安全的i++,保证复合操作的原子性

注意:使用Runnable让两个线程关注一个同一个对象锁
错误示范:

使用synchronized的第三种方法进行修正

  • synchronized除了线程同步、确保线程安全,还可以保证线程间的可见性和有序性(多个线程串行执行)

程序中的幽灵:隐蔽的错误

出现异常的错误至少可以发现,没有异常的错误例如数据溢出就很难排查

无提示的错误案例

并发下的ArrayList

例子:

可能出现的三种情况

  • 程序正常结束
  • 程序抛出数组越界异常
  • 没有提示的错误
    改进方法:使用线程安全的Vector代替ArrayList

并发下诡异的HashMap

HashMap的源码解析
Jdk 8以前为什么 HashMap在多线程下的put操作容易导致链表成环

初学者常见问题:错误的加锁

public class BadLockOnInteger implements Runnable{
    public static Integer i=0;
    static BadLockOnInteger instance = new BadLockOnInteger();
    @Override
    public void run(){
        for(int j=0;j<10000000;j++){
            synchronized(i){
                i++;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t1 = new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

为什么最终的结果不是20000000?

在Java中Integer属于不变对象,每次对Integer对象进行加法操作时,实际上是新创建了一个Integer对象来表示加完后的结果,因此i++的本质是创建了一个新的Integer对象,并将其引用赋给i,所以在多个线程间,并不一定能够看到同一个对象,代码中两个线程每次加锁可能都在了不同的对象实例上
如何修正?

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

推荐阅读更多精彩内容