JAVA高并发实战——Java并行程序基础

一、线程与进程的区别

  • 线程就是轻量级的进程,是程序执行的最小单位。使用多线程而不是用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。
  • 线程的生命周期


    在这里插入图片描述

    1、NEW状态:刚刚创建线程,线程还没开始执行。调用start(),线程开始执行
    2、RUNNABLE状态:表示线程所需的一切资源已经准备好了。
    3、BLOCKED状态:如果线程在执行过程中遇到了synchronized同步块,就会进入 BOLCKED阻塞状态,这时线程会暂停执行,直到获得请求的锁
    4、WAITING状态:进入一个无时间限制的等待。WAINTING和TIMED_WAITING的线程一般是在等待一些特殊的事件,比如wait()方法等待的线程等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。
    5、TIMED_WAITING状态:会进入一个有时限的等待。
    6、RUNNABLE状态:等到了期望的时间,线程会继续执行,进入RUNNABLE状态
    7、TERMINATED状态:结束。
    ==注意:从NEW状态出发后,线程不能再回到NEW状态,同理,对于TERMINATED状态的线程也不能再回到RUNNABLE状态。==

二、线程的基本操作

  • 新建线程
    1、重写Thread的run()方法
public class Test01 {
    public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                System.out.println("Hello");
            }
        };
        t1.start();
    }
}

         2、实现Runnable接口(更为合理)

public class Test02 implements Runnable {

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

    @Override
    public void run() {
        System.out.println("Oh, I am Runnable");
    }
}
  • 终止线程
    不要使用stop()方法直接终止线程,并且这个方法已经废弃。
    Thread.stop()方法会直接终止线程,并立即释放这个线程所持有的锁,无法保证对象的一致性
    正确的终止方法:(设置一个标志变量stopme)
 public static class ChangeObjectThread extends Thread {
        volatile boolean stopme = false;

        public void stopMe() {
            stopme = true;
        }

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

               synchronized (u) {
                   int v = (int)(System.currentTimeMillis() / 1000);
                   u.setId(v);
                   //do something
                   try {
                       Thread.sleep(100);
                   }catch (InterruptedException e) {
                       e.printStackTrace();
                   }

                   u.setName(String.valueOf(v));
               }
               Thread.yield();
           }
        }
    }
  • 线程中断
    线程中断是一种重要的线程协作机制。线程中断并不会使线程立即退出,而是给线程发送一个通知,通知目标线程,有人希望你退出啦。至于目标线程接到通知后如何处理,则完全由目标线程自行决定。
    有三个方法和线程中断有关:


    在这里插入图片描述

    判断线程中断并处理完事情后退出的正确方法:

public class Test04 {
    public static void main(String[] args) throws InterruptedException {
        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(1000);
        t1.interrupt();
    }
}

注意:Thread.sleep()方法由于中断而抛出异常,此时,它会清楚中断标记,如果不加处理,那么下一次循坏开始时,就无话捕获这个中断,故在异常处理中,再次设置中断标志位。

  • 等待(wait)和通知(notify)
    1、JDK提供了两个非常重要的接口线程:等待wait()方法和通知notify()方法。这两个恶方法并不是在Thread类中的,而是在Object类。意味着所有对象都可以调用这两个方法。
    2、工作实现:比如,在线程A中,调用了obj.wait()方法,那么线程A就会停止继续执行,转为等待状态。等待到何时结束呢?线程A会一直等到其他线程调用了obj.notify()方法为止。
    3、工作原理:如果一个线程调用了obj.wait()方法,那么它就会进入object对象的等待队列。这个等待队列中,可能会有多线程,因为系统运行多个线程同时等待某一个对象。当object.notify()方法被调用时,它就会从这个等待队列中随机选择一个线程,并将其唤醒。需要注意的是,这个选择是不公平的,是随机唤醒某一个线程。


    在这里插入图片描述

    4、Object对象还有一个类似的notifyAll()方法,它会唤醒等待队列中所有的线程。
    5、这里举出一个简单的案例:

public class SimpleWN {
    final static Object object = new Object();

    public static class T1 extends Thread {
        public void run() {
            synchronized (object) {
                System.out.println(System.currentTimeMillis() + ":T1 start!");
                try {
                    System.out.println(System.currentTimeMillis() + ":T1 wait for object");
                    object.wait();
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis() + ":T1 ends");
            }
        }
    }

    public static class T2 extends Thread {
        public void run() {
            synchronized (object) {
                System.out.println(System.currentTimeMillis() + ":T2 start!notify one thread");
                object.notify();
                System.out.println(System.currentTimeMillis() + ":T2 ends");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {

                }
            }
        }
    }

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

在这里插入图片描述

注意:Object.wait()方法和Thread.sleep()方法都可以让线程等待若干时间。除wait()方法可以被唤醒外。另外一个主要区别就是wait()方法会释放目标对象的锁、而Thread.sleep()方法不会释放任何资源。

  • 等待线程结束(join)和谦让(yield)
    join()方法:第一个join()方法表示无限等待,它会一直阻塞当前线程,直到目标线程执行完毕。第二个方法给出一个最大等待时间,如果超过给定时间目标线程还在执行,当前线程就不再等了,继续往下执行。


    在这里插入图片描述

    yield()方法:yield方法会使当前线程主动让出CPU。但是要注意,让出CPU后,还是会进行CPU资源的争夺。如果你觉得一个线程不那么重要,或者优先级非常低,而且又还怕它占用过多资源,就可以使用yield()方法,给其他重要的线程先工作。

    三、volatile与Java内存模型(JMM)

    Volatile:通过加入内存屏障和禁止重排序优化来实现
  • 对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存。
在这里插入图片描述
  • 对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。


    在这里插入图片描述
  • volatile可以保证可见性但是不能保证原子性。例如下面的代码:

public class CountExample {

    //请求总数
    public static int clientTotal  = 5000;
    //同时并发执行的线程数
    public static int threadTotal = 200;
    //计数 *
    public static volatile int count = 0;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();//创建线程池
        final Semaphore semaphore = new Semaphore(threadTotal);//定义信号量,给出允许并发的数目
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);//定义计数器闭锁
        for (int i = 0;i<clientTotal;i++){
            executorService.execute(()->{
                try {
                    semaphore.acquire();//判断进程是否允许被执行
                    add();
                    semaphore.release();//释放进程
                } catch (InterruptedException e) {
                    log.error("excption",e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();//保证信号量减为0
        executorService.shutdown();//关闭线程池
        log.info("count:{}",count);
    }

    private static void add(){
        count++;
    }
}

多次运行代码我们发现:count的最终结果并不是预期的5000,而是有时为5000,但是大多数时间比5000小,这是为什么呢?原因在于对count++的操作中,jvm对count做了三步操作:

1、从主存中取出count的值放入工作变量 count 
2、对工作变量中的count进行+1 
3、将工作变量中的count刷新回主存中

在单线程执行此操作绝对没有问题,但是在多线程环境中,假设有两个线程A、B同时执行count++操作,某一刻A与B同时读取主存中count的值,然后在自己线程对应的工作空间中对count+1,最后又同时将count+1的值写回主存。到此,count+1的值被写回主存两遍,所以导致最终的count值小了1。在整体程序执行过程中,该事件发生一次或多次,自然结果就不正确。
那么volatile适合做什么呢?其实它比较适合做状态标记量(不会涉及到多线程同时读写的操作),而且要保证两点:
(1)对变量的写操作不依赖于当前值
(2)该变量没有包含在具有其他变量的不变的式子中

package chapter2;

/**
 * NoVisibility class
 *
 * @author Flc
 * @date 2019/7/12
 */
public class NoVisibility {
    private volatile static boolean ready = false;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            while(!ready) {
                System.out.println(number);
            }
            System.out.println("ok");
        }
    }
    private static class ReaderThread2 extends Thread {
        public void run() {
            try {
                Thread.sleep(2000);
                ready = true;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ReaderThread().start();
        new ReaderThread2().start();
    }
}

四、分门别类的管理:线程组

  • 在一个系统中,如果线程数量很多,而且功能分配比较明确,就可以将相同功能的线程放在同一个线程组里。
  • 线程组实现较为简单,具体实现如下:
public class ThreadGroupName implements Runnable {
    public static void main(String[] args) {
        ThreadGroup tg = new ThreadGroup("PringGroup");
        Thread thread1 = new Thread(tg, new ThreadGroupName(), "T1");
        Thread thread2 = new Thread(tg, new ThreadGroupName(), "T2");
        thread1.start();
        thread2.start();
        System.out.println(tg.activeCount());
        tg.list();
    }

    @Override
    public void run() {
        String groupAndName = Thread.currentThread().getThreadGroup().getName()
                + "-" + Thread.currentThread().getName();

        while (true) {
            System.out.println("I am " + groupAndName);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

五、驻守后台:守护进程(Daemon)

  • 守护进程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默完成一些系统性服务。比如垃圾回收线程,JIT线程就可以理解为守护线程。
  • 与之相对应的是用户线程,用户线程可以认为是系统的工作线程,它会完成这个程序应该要完成的业务操作
  • 如果用户线程全部结束,则意味着这个程序实际上上无视可做了,守护进程要守护的对象也就不存在了,那么整个应用就结束了。
  • 下面简单实现一下守护进程
public class DaemonDemo {

    public static class DaemonT extends Thread {

        public void run() {
            while(true) {
                System.out.println("I am alive");
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = new DaemonT();
        t.setDaemon(true);
        t.start();
        Thread.sleep(2000);
    }
}

六、先做重要的事:线程优先级

  • Java中的线程可以有自己的优先级。优先级高的线程在竞争资源时会更有优势,更可能抢占资源。(优先级高也不一定意味着一定可以抢占资源)

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

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
  • 数字越大则线程优先级越高。例如下面的例子:
public class PriorityDemo {
    public static class HightPriority extends Thread {
        static int count = 0;
        public void run() {
            while(true) {
                synchronized (PriorityDemo.class) {
                    count++;
                    if(count > 100000) {
                        System.out.println("High Priority is complete");
                        break;
                    }
                }
            }
        }
    }

    public static  class LowPriority extends Thread {
        static int count = 0;
        public void run () {
            while (true) {
                synchronized (PriorityDemo.class) {
                    count++;
                    if(count > 100000) {
                        System.out.println("Low Priority is complete");
                        break;
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        HightPriority high = new HightPriority();
        LowPriority low =new LowPriority();
        high.setPriority(Thread.MAX_PRIORITY);
        low.setPriority(Thread.MIN_PRIORITY);
        low.start();
        high.start();
    }
}

七、线程安全的概念与关键字syschronized

  • 程序并行化是为了获得更高的执行效率,但前提是,高效率不能以牺牲正确性为代价。
  • volatile关键字并不能保证线程安全。它只能保证一个线程修改数据之后,其他线程能够看到这个改动。
  • 关键字synchronized的作用是实现线程之间的同步。它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性。
  • synchronized关键字有如下几种用法
    1、指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
public class AccountingSync implements Runnable {

    static AccountingSync instance = new AccountingSync();

    static int j = 0;

    @Override
    public void run() {
        for(int i = 0; i < 100000; i++) {
            synchronized (instance) {
                j++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(j);
    }
}

2、直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁

public class AccountingSync2 implements Runnable {
    static AccountingSync2 instance = new AccountingSync2();
    static int i = 0;
    public synchronized void increase() {
        i++;
    }


    @Override
    public void run() {
        for(int j = 0; j < 100000; j++) {
            increase();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

3、直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。

public static synchronized void increase(){
    i++;
}
  • 关键字synchronized可以保证线程安全,可见性和有序性。可以完全替代volatile关键字
  • 线程解锁前,必须把共享变量的最新值刷新到主内存
  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁是同一把锁)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容

  • layout: posttitle: 《Java并发编程的艺术》笔记categories: Javaexcerpt...
    xiaogmail阅读 5,793评论 1 19
  • 线程池ThreadPoolExecutor corepoolsize:核心池的大小,默认情况下,在创建了线程池之后...
    irckwk1阅读 713评论 0 0
  • 第2章 java并发机制的底层实现原理 Java中所使用的并发机制依赖于JVM的实现和CPU的指令。 2.1 vo...
    kennethan阅读 1,391评论 0 2
  • 本文出自 Eddy Wiki ,转载请注明出处:http://eddy.wiki/interview-java.h...
    eddy_wiki阅读 2,054评论 0 14
  • Java SE 基础: 封装、继承、多态 封装: 概念:就是把对象的属性和操作(或服务)结合为一个独立的整体,并尽...
    Jayden_Cao阅读 2,099评论 0 8