精华内容
下载资源
问答
  • 多线程并发编程

    2018-09-26 19:06:46
    多线程并发编程 前言 多线程并发编程是Java编程中重要的一块内容,也是面试重点覆盖区域,所以学好多线程并发编程对我们来说极其重要,下面跟我一起开启本次的学习之旅吧。 正文 线程与进程 1 线程:进程中负责...

    多线程并发编程

    前言

    多线程并发编程是Java编程中重要的一块内容,也是面试重点覆盖区域,所以学好多线程并发编程对我们来说极其重要,下面跟我一起开启本次的学习之旅吧。

    正文

    线程与进程

    1 线程:进程中负责程序执行的执行单元
    线程本身依靠程序进行运行
    线程是程序中的顺序控制流,只能使用分配给程序的资源和环境

    2 进程:执行中的程序
    一个进程至少包含一个线程

    3 单线程:程序中只存在一个线程,实际上主方法就是一个主线程

    4 多线程:在一个程序中运行多个任务
    目的是更好地使用CPU资源

    线程的实现

    继承Thread类

    java.lang包中定义, 继承Thread类必须重写run()方法

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    class MyThread extends Thread{

        private static int num = 0;

     

        public MyThread(){

            num++;

        }

     

        @Override

        public void run() {

            System.out.println("主动创建的第"+num+"个线程");

        }

    }

    创建好了自己的线程类之后,就可以创建线程对象了,然后通过start()方法去启动线程。注意,不是调用run()方法启动线程,run方法中只是定义需要执行的任务,如果调用run方法,即相当于在主线程中执行run方法,跟普通的方法调用没有任何区别,此时并不会创建一个新的线程来执行定义的任务。

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    public class Test {

        public static void main(String[] args)  {

            MyThread thread = new MyThread();

            thread.start();

        }

    }

    class MyThread extends Thread{

        private static int num = 0;

        public MyThread(){

            num++;

        }

        @Override

        public void run() {

            System.out.println("主动创建的第"+num+"个线程");

        }

    }

    在上面代码中,通过调用start()方法,就会创建一个新的线程了。为了分清start()方法调用和run()方法调用的区别,请看下面一个例子:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    public class Test {

        public static void main(String[] args)  {

            System.out.println("主线程ID:"+Thread.currentThread().getId());

            MyThread thread1 = new MyThread("thread1");

            thread1.start();

            MyThread thread2 = new MyThread("thread2");

            thread2.run();

        }

    }

     

    class MyThread extends Thread{

        private String name;

     

        public MyThread(String name){

            this.name = name;

        }

     

        @Override

        public void run() {

            System.out.println("name:"+name+" 子线程ID:"+Thread.currentThread().getId());

        }

    }

    运行结果:

     

     

    从输出结果可以得出以下结论:

    1)thread1和thread2的线程ID不同,thread2和主线程ID相同,说明通过run方法调用并不会创建新的线程,而是在主线程中直接运行run方法,跟普通的方法调用没有任何区别;

    2)虽然thread1的start方法调用在thread2的run方法前面调用,但是先输出的是thread2的run方法调用的相关信息,说明新线程创建的过程不会阻塞主线程的后续执行。

    实现Runnable接口

    在Java中创建线程除了继承Thread类之外,还可以通过实现Runnable接口来实现类似的功能。实现Runnable接口必须重写其run方法。
    下面是一个例子:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    public class Test {

        public static void main(String[] args)  {

            System.out.println("主线程ID:"+Thread.currentThread().getId());

            MyRunnable runnable = new MyRunnable();

            Thread thread = new Thread(runnable);

            thread.start();

        }

    }

    class MyRunnable implements Runnable{

        public MyRunnable() {

        }

     

        @Override

        public void run() {

            System.out.println("子线程ID:"+Thread.currentThread().getId());

        }

    }

    Runnable的中文意思是“任务”,顾名思义,通过实现Runnable接口,我们定义了一个子任务,然后将子任务交由Thread去执行。注意,这种方式必须将Runnable作为Thread类的参数,然后通过Thread的start方法来创建一个新线程来执行该子任务。如果调用Runnable的run方法的话,是不会创建新线程的,这根普通的方法调用没有任何区别。

    事实上,查看Thread类的实现源代码会发现Thread类是实现了Runnable接口的。

    在Java中,这2种方式都可以用来创建线程去执行子任务,具体选择哪一种方式要看自己的需求。直接继承Thread类的话,可能比实现Runnable接口看起来更加简洁,但是由于Java只允许单继承,所以如果自定义类需要继承其他类,则只能选择实现Runnable接口。

    使用ExecutorService、Callable、Future实现有返回结果的多线程

    多线程后续会学到,这里暂时先知道一下有这种方法即可。

    ExecutorService、Callable、Future这个对象实际上都是属于Executor框架中的功能类。想要详细了解Executor框架的可以访问http://www.javaeye.com/topic/366591 ,这里面对该框架做了很详细的解释。返回结果的线程是在JDK1.5中引入的新特征,确实很实用,有了这种特征我就不需要再为了得到返回值而大费周折了,而且即便实现了也可能漏洞百出。

    可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。下面提供了一个完整的有返回结果的多线程测试例子,在JDK1.5下验证过没问题可以直接使用。代码如下:

    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

    32

    33

    34

    35

    36

    37

    38

    39

    40

    41

    42

    43

    44

    45

    46

    47

    48

    49

    50

    51

    52

    53

    54

    /**

    * 有返回值的线程

    */ 

    @SuppressWarnings("unchecked"

    public class Test { 

    public static void main(String[] args) throws ExecutionException, 

        InterruptedException { 

       System.out.println("----程序开始运行----"); 

       Date date1 = new Date(); 

     

       int taskSize = 5

       // 创建一个线程池 

       ExecutorService pool = Executors.newFixedThreadPool(taskSize); 

       // 创建多个有返回值的任务 

       List<Future> list = new ArrayList<Future>(); 

       for (int i = 0; i < taskSize; i++) { 

        Callable c = new MyCallable(i + " "); 

        // 执行任务并获取Future对象 

        Future f = pool.submit(c); 

        // System.out.println(">>>" + f.get().toString()); 

        list.add(f); 

       

       // 关闭线程池 

       pool.shutdown(); 

     

       // 获取所有并发任务的运行结果 

       for (Future f : list) { 

        // 从Future对象上获取任务的返回值,并输出到控制台 

        System.out.println(">>>" + f.get().toString()); 

       

     

       Date date2 = new Date(); 

       System.out.println("----程序结束运行----,程序运行时间【" 

         + (date2.getTime() - date1.getTime()) + "毫秒】"); 

     

    class MyCallable implements Callable<Object> { 

    private String taskNum; 

     

    MyCallable(String taskNum) { 

       this.taskNum = taskNum; 

     

    public Object call() throws Exception { 

       System.out.println(">>>" + taskNum + "任务启动"); 

       Date dateTmp1 = new Date(); 

       Thread.sleep(1000); 

       Date dateTmp2 = new Date(); 

       long time = dateTmp2.getTime() - dateTmp1.getTime(); 

       System.out.println(">>>" + taskNum + "任务终止"); 

       return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】"

    }

    }

    代码说明:
    上述代码中Executors类,提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。
    public static ExecutorService newFixedThreadPool(int nThreads)
    创建固定数目线程的线程池。

    public static ExecutorService newCachedThreadPool()
    创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。

    public static ExecutorService newSingleThreadExecutor()
    创建一个单线程化的Executor。

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
    创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

    ExecutoreService提供了submit()方法,传递一个Callable,或Runnable,返回Future。如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。

    线程的状态

    在正式学习Thread类中的具体方法之前,我们先来了解一下线程有哪些状态,这个将会有助于后面对Thread类中的方法的理解。

    • 创建(new)状态: 准备好了一个多线程的对象
    • 就绪(runnable)状态: 调用了start()方法, 等待CPU进行调度
    • 运行(running)状态: 执行run()方法
    • 阻塞(blocked)状态: 暂时停止执行, 可能将资源交给其它线程使用
    • 终止(dead)状态: 线程销毁

    当需要新起一个线程来执行某个子任务时,就创建了一个线程。但是线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源,在前面的JVM内存区域划分一篇博文中知道程序计数器、Java栈、本地方法栈都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态。

    当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。

    线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块给阻塞,此时就对应着多个状态:time waiting(睡眠或等待一定的事件)、waiting(等待被唤醒)、blocked(阻塞)。

    当由于突然中断或者子任务执行完毕,线程就会被消亡。

    下面这副图描述了线程从创建到消亡之间的状态:

    在有些教程上将blocked、waiting、time waiting统称为阻塞状态,这个也是可以的,只不过这里我想将线程的状态和Java中的方法调用联系起来,所以将waiting和time waiting两个状态分离出来。

    注:sleep和wait的区别:

    • sleepThread类的方法,waitObject类中定义的方法.
    • Thread.sleep不会导致锁行为的改变, 如果当前线程是拥有锁的, 那么Thread.sleep不会让线程释放锁.
    • Thread.sleepObject.wait都会暂停当前的线程. OS会将执行时间分配给其它线程. 区别是, 调用wait后, 需要别的线程执行notify/notifyAll才能够重新获得CPU执行时间.

    上下文切换

    对于单核CPU来说(对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。

    由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。举个简单的例子:比如一个线程A正在读取一个文件的内容,正读到文件的一半,此时需要暂停线程A,转去执行线程B,当再次切换回来执行线程A的时候,我们不希望线程A又从文件的开头来读取。

    因此需要记录线程A的运行状态,那么会记录哪些数据呢?因为下次恢复时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。

    说简单点的:对于线程的上下文切换实际上就是 存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行

    虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。

    线程的常用方法

    编号 方法 说明
    1 public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
    2 public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
    3 public final void setName(String name) 改变线程名称,使之与参数 name 相同。
    4 public final void setPriority(int priority) 更改线程的优先级。
    5 public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。
    6 public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。
    7 public void interrupt() 中断线程。
    8 public final boolean isAlive() 测试线程是否处于活动状态。
    9 public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。
    10 public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
    11 public static Thread currentThread() 返回对当前正在执行的线程对象的引用。

     

     

    静态方法

    currentThread()方法

    currentThread()方法可以返回代码段正在被哪个线程调用的信息。

    1

    2

    3

    4

    5

    public class Run1{

        public static void main(String[] args){                

        System.out.println(Thread.currentThread().getName());

        }

    }

    sleep()方法

    方法sleep()的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个“正在执行的线程”是指this.currentThread()返回的线程。

    sleep方法有两个重载版本:

    1

    2

    sleep(long millis)     //参数为毫秒

    sleep(long millis,int nanoseconds)    //第一参数为毫秒,第二个参数为纳秒

    sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
    但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。看下面这个例子就清楚了:

    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

    32

    public class Test {

     

        private int i = 10;

        private Object object = new Object();

     

        public static void main(String[] args) throws IOException  {

            Test test = new Test();

            MyThread thread1 = test.new MyThread();

            MyThread thread2 = test.new MyThread();

            thread1.start();

            thread2.start();

        }

     

        class MyThread extends Thread{

            @Override

            public void run() {

                synchronized (object) {

                    i++;

                    System.out.println("i:"+i);

                    try {

                        System.out.println("线程"+Thread.currentThread().getName()+"进入睡眠状态");

                        Thread.currentThread().sleep(10000);

                    } catch (InterruptedException e) {

                        // TODO: handle exception

                    }

                    System.out.println("线程"+Thread.currentThread().getName()+"睡眠结束");

                    i++;

                    System.out.println("i:"+i);

                }

            }

        }

    }

    输出结果:

    从上面输出结果可以看出,当Thread-0进入睡眠状态之后,Thread-1并没有去执行具体的任务。只有当Thread-0执行完之后,此时Thread-0释放了对象锁,Thread-1才开始执行。

    注意,如果调用了sleep方法,必须捕获InterruptedException异常或者将该异常向上层抛出。当线程睡眠时间满后,不一定会立即得到执行,因为此时可能CPU正在执行其他的任务。所以说调用sleep方法相当于让线程进入阻塞状态。

    yield()方法

    调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。

    注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
    代码:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    public class MyThread  extends Thread{

        @Override

        public void run() {

            long beginTime=System.currentTimeMillis();

            int count=0;

            for (int i=0;i<50000000;i++){

                count=count+(i+1);

                //Thread.yield();

            }

            long endTime=System.currentTimeMillis();

            System.out.println("用时:"+(endTime-beginTime)+" 毫秒!");

        }

    }

     

    public class Run {

        public static void main(String[] args) {

            MyThread t= new MyThread();

            t.start();

        }

    }

    执行结果:

    1

    用时:3 毫秒!

    如果将 //Thread.yield();的注释去掉,执行结果如下:

    1

    用时:16080 毫秒!

    对象方法

    start()方法

    start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。

    run()方法

    run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。

    getId()

    getId()的作用是取得线程的唯一标识
    代码:

    1

    2

    3

    4

    5

    6

    public class Test {

        public static void main(String[] args) {

            Thread t= Thread.currentThread();

            System.out.println(t.getName()+" "+t.getId());

        }

    }

    输出:

    1

    main 1

    isAlive()方法

    方法isAlive()的功能是判断当前线程是否处于活动状态
    代码:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    public class MyThread  extends Thread{

        @Override

        public void run() {

            System.out.println("run="+this.isAlive());

        }

    }

    public class RunTest {

        public static void main(String[] args) throws InterruptedException {

            MyThread myThread=new MyThread();

            System.out.println("begin =="+myThread.isAlive());

            myThread.start();

            System.out.println("end =="+myThread.isAlive());

        }

    }

    程序运行结果:

    1

    2

    3

    begin ==false

    run=true

    end ==false

    方法isAlive()的作用是测试线程是否偶处于活动状态。什么是活动状态呢?活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程是“存活”的。
    有个需要注意的地方

    1

    System.out.println("end =="+myThread.isAlive());

    虽然上面的实例中打印的值是true,但此值是不确定的。打印true值是因为myThread线程还未执行完毕,所以输出true。如果代码改成下面这样,加了个sleep休眠:

    1

    2

    3

    4

    5

    6

    7

    public static void main(String[] args) throws InterruptedException {

            MyThread myThread=new MyThread();

            System.out.println("begin =="+myThread.isAlive());

            myThread.start();

            Thread.sleep(1000);

            System.out.println("end =="+myThread.isAlive());

        }

    则上述代码运行的结果输出为false,因为mythread对象已经在1秒之内执行完毕。

    join()方法

    在很多情况下,主线程创建并启动了线程,如果子线程中药进行大量耗时运算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完成之后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到join()方法了。方法join()的作用是等待线程对象销毁。

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    public class Thread4 extends Thread{

        public Thread4(String name) {

            super(name);

        }

        public void run() {

            for (int i = 0; i < 5; i++) {

                System.out.println(getName() + "  " + i);

            }

        }

        public static void main(String[] args) throws InterruptedException {

            // 启动子进程

            new Thread4("new thread").start();

            for (int i = 0; i < 10; i++) {

                if (i == 5) {

                    Thread4 th = new Thread4("joined thread");

                    th.start();

                    th.join();

                }

                System.out.println(Thread.currentThread().getName() + "  " + i);

            }

        }

    }

    执行结果:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    main  0

    main  1

    main  2

    main  3

    main  4

    new thread  0

    new thread  1

    new thread  2

    new thread  3

    new thread  4

    joined thread  0

    joined thread  1

    joined thread  2

    joined thread  3

    joined thread  4

    main  5

    main  6

    main  7

    main  8

    main  9

    由上可以看出main主线程等待joined thread线程先执行完了才结束的。如果把th.join()这行注释掉,运行结果如下:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    main  0

    main  1

    main  2

    main  3

    main  4

    main  5

    main  6

    main  7

    main  8

    main  9

    new thread  0

    new thread  1

    new thread  2

    new thread  3

    new thread  4

    joined thread  0

    joined thread  1

    joined thread  2

    joined thread  3

    joined thread  4

    getName和setName

    用来得到或者设置线程名称。

    getPriority和setPriority

    用来获取和设置线程优先级。

    setDaemon和isDaemon

    用来设置线程是否成为守护线程和判断线程是否是守护线程。

    守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。

    在上面已经说到了Thread类中的大部分方法,那么Thread类中的方法调用到底会引起线程状态发生怎样的变化呢?下面一幅图就是在上面的图上进行改进而来的:

     

     

    停止线程

    停止线程是在多线程开发时很重要的技术点,掌握此技术可以对线程的停止进行有效的处理。
    停止一个线程可以使用Thread.stop()方法,但最好不用它。该方法是不安全的,已被弃用。
    在Java中有以下3种方法可以终止正在运行的线程:

    • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
    • 使用stop方法强行终止线程,但是不推荐使用这个方法,因为stop和suspend及resume一样,都是作废过期的方法,使用他们可能产生不可预料的结果。
    • 使用interrupt方法中断线程,但这个不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。

    暂停线程

    interrupt()方法

    线程的优先级

    在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务。
    设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。
    设置线程的优先级使用setPriority()方法,此方法在JDK的源码如下:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    public final void setPriority(int newPriority) {

            ThreadGroup g;

            checkAccess();

            if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {

                throw new IllegalArgumentException();

            }

            if((g = getThreadGroup()) != null) {

                if (newPriority > g.getMaxPriority()) {

                    newPriority = g.getMaxPriority();

                }

                setPriority0(priority = newPriority);

            }

        }

    在Java中,线程的优先级分为1~10这10个等级,如果小于1或大于10,则JDK抛出异常throw new IllegalArgumentException()。
    JDK中使用3个常量来预置定义优先级的值,代码如下:

    1

    2

    3

    public final static int MIN_PRIORITY = 1;

    public final static int NORM_PRIORITY = 5;

    public final static int MAX_PRIORITY = 10;

    线程优先级特性:

    • 继承性
      比如A线程启动B线程,则B线程的优先级与A是一样的。
    • 规则性
      高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。
    • 随机性
      优先级较高的线程不一定每一次都先执行完。

    守护线程

    在Java线程中有两种线程,一种是User Thread(用户线程),另一种是Daemon Thread(守护线程)。
    Daemon的作用是为其他线程的运行提供服务,比如说GC线程。其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开:如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了。

    守护线程并非虚拟机内部可以提供,用户也可以自行的设定守护线程,方法:public final void setDaemon(boolean on) ;但是有几点需要注意:

    • thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。 (备注:这点与守护进程有着明显的区别,守护进程是创建后,让进程摆脱原会话的控制+让进程摆脱原进程组的控制+让进程摆脱原控制终端的控制;所以说寄托于虚拟机的语言机制跟系统级语言有着本质上面的区别)
    • 在Daemon线程中产生的新线程也是Daemon的。 (这一点又是有着本质的区别了:守护进程fork()出来的子进程不再是守护进程,尽管它把父进程的进程相关信息复制过去了,但是子进程的进程的父进程不是init进程,所谓的守护进程本质上说就是“父进程挂掉,init收养,然后文件0,1,2都是/dev/null,当前目录到/”)
    • 不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑。因为在Daemon Thread还没来的及进行操作时,虚拟机可能已经退出了。

    同步与死锁

    1. 同步代码块
      在代码块上加上”synchronized”关键字,则此代码块就称为同步代码块
    2. 同步代码块格式

      1

      2

      3

      synchronized(同步对象){

       需要同步的代码块;

      }

    3. 同步方法
      除了代码块可以同步,方法也是可以同步的
    4. 方法同步格式

      1

      synchronized void 方法名称(){}

      synchronized后续会单独来学习。(●’◡’●)

    面试题

    线程和进程有什么区别?
    答:一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。而线程是在进程中执行的一个任务。线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。

    如何在Java中实现线程?
    答:
    创建线程有两种方式:
    一、继承 Thread 类,扩展线程。
    二、实现 Runnable 接口。

    启动一个线程是调用run()还是start()方法?
    答:启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM 调度并执行,这并不意味着线程就会立即运行。run()方法是线程启动后要进行回调(callback)的方法。

    Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行,它们有什么区别?
    答:sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态,请参考第66题中的线程状态转换图)。wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。

    线程的sleep()方法和yield()方法有什么区别?
    答:
    ① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
    ② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
    ③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
    ④ sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。

    请说出与线程同步以及线程调度相关的方法。
    答:

    • wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
    • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
    • notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
    • notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

    总结

    以上就是多线程的一些基础概念,可能总结的不够仔细,多多包涵。后续会针对一些比较重要的知识点单独列出来总结。学好多线程是拿高薪的基础,小伙伴一起加油吧!

    参考

    该文为本人学习的笔记,方便以后自己跳槽前复习。参考网上各大帖子,取其精华整合自己的理解而成。还有,关注我个人主页的公众号,里面电子书资源有《Java多线程编程核心技术》以及《JAVA并发编程实践》高清版,需要的小伙伴自己取。

    《Java多线程编程核心技术》
    《JAVA并发编程实践》
    Java并发编程:Thread类的使用
    关于Java并发编程的总结和思考
    JAVA多线程实现的三种方式

    整理的思维导图

    个人整理的多线程基础的思维导图,导出的图片无法查看备注的一些信息,所以需要源文件的童鞋可以关注我个人主页上的公众号,回复多线程基础即可获取源文件。

     

    展开全文
  • Java 多线程 并发编程

    千次阅读 2016-04-21 20:34:49
    Java 多线程 并发编程

    一、多线程
    1、操作系统有两个容易混淆的概念,进程和线程。
    进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据;不同进程的地址空间是互相隔离的;进程拥有各种资源和状态信息,包括打开的文件、子进程和信号处理。
    线程:表示程序的执行流程,是CPU调度执行的基本单位;线程有自己的程序计数器、寄存器、堆栈和帧。同一进程中的线程共用相同的地址空间,同时共享进进程锁拥有的内存和其他资源。
    2、Java标准库提供了进程和线程相关的API,进程主要包括表示进程的java.lang.Process类和创建进程的java.lang.ProcessBuilder类;
    表示线程的是java.lang.Thread类,在虚拟机启动之后,通常只有Java类的main方法这个普通线程运行,运行时可以创建和启动新的线程;还有一类守护线程(damon thread),守护线程在后台运行,提供程序运行时所需的服务。当虚拟机中运行的所有线程都是守护线程时,虚拟机终止运行。
    3、线程间的可见性:一个线程对进程中共享的数据的修改,是否对另一个线程可见
    可见性问题:
    a、CPU采用时间片轮转等不同算法来对线程进行调度

    public class IdGenerator{
       private int value = 0;
       public int getNext(){
          return value++;
       }
    }

    对于IdGenerator的getNext()方法,在多线程下不能保证返回值是不重复的:各个线程之间相互竞争CPU时间来获取运行机会,CPU切换可能发生在执行间隙。
    以上代码getNext()的指令序列:CPU切换可能发生在7条指令之间,多个getNext的指令交织在一起。

    aload_0
    dup
    getfield #12
    dup_x1
    iconst_1
    iadd
    putfield #12

    b、CPU缓存:
    目前CPU一般采用层次结构的多级缓存的架构,有的CPU提供了L1、L2和L3三级缓存。当CPU需要读取主存中某个位置的数据时,会一次检查各级缓存中是否存在对应的数据。如果有,直接从缓存中读取,这比从主存中读取速度快很多。当CPU需要写入时,数据先被写入缓存中,之后再某个时间点写回主存。所以某些时间点上,缓存中的数据与主存中的数据可能是不一致。
    c、指令顺序重排
    出行性能考虑,编译器在编译时可能会对字节代码的指令顺序进行重新排列,以优化指令的执行顺序,在单线程中不会有问题,但在多线程可能产生与可见性相关的问题。
    二、Java内存模型(Java Memory Model)
    屏蔽了CPU缓存等细节,只关注主存中的共享变量;关注对象的实例域、静态域和数组元素;关注线程间的动作。
    1、volatile关键词:用来对共享变量的访问进行同步,上一次写入操作的结果对下一次读取操作是肯定可见的。(在写入volatile变量值之后,CPU缓存中的内容会被写回内存;在读取volatile变量时,CPU缓存中的对应内容会被置为失效,重新从主存中进行读取),volatile不使用锁,性能优于synchronized关键词。
    用来确保对一个变量的修改被正确地传播到其他线程中。
    例子:A线程是Worker,一直跑循环,B线程调用setDone(true),A线程即停止任务

    public class Worker{
       private volatile boolean done;
       public void setDone(boolean done){
          this.done = done;
       }
       public void work(){
          while(!done){
             //执行任务;
          }
       }
    }

    例子:错误使用。因为没有锁的支持,volatile的修改不能依赖于当前值,当前值可能在其他线程中被修改。(Worker是直接赋新值与当前值无关)

    public class Counter {
        public volatile static int count = 0;
        public static void inc() {
            //这里延迟1毫秒,使得结果明显
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
            }
            count++;
        }
        public static void main(String[] args) {
            //同时启动1000个线程,去进行i++计算,看看实际结果
            for (int i = 0; i < 1000; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Counter.inc();
                    }
                }).start();
            }
            //这里每次运行的值都有可能不同,可能不为1000
            System.out.println("运行结果:Counter.count=" + Counter.count);
        }
    }

    2、final关键词
    final关键词声明的域的值只能被初始化一次,一般在构造方法中初始化。。(在多线程开发中,final域通常用来实现不可变对象)
    当对象中的共享变量的值不可能发生变化时,在多线程中也就不需要同步机制来进行处理,故在多线程开发中应尽可能使用不可变对象。
    另外,在代码执行时,final域的值可以被保存在寄存器中,而不用从主存中频繁重新读取。
    3、java基本类型的原子操作
    1)基本类型,引用类型的复制引用是原子操作;(即一条指令完成)
    2)long与double的赋值,引用是可以分割的,非原子操作;
    3)要在线程间共享long或double的字段时,必须在synchronized中操作,或是声明成volatile
    三、Java提供的线程同步方式
    1、synchronized关键字
    方法或代码块的互斥性来完成实际上的一个原子操作。(方法或代码块在被一个线程调用时,其他线程处于等待状态)
    所有的Java对象都有一个与synchronzied关联的监视器对象(monitor),允许线程在该监视器对象上进行加锁和解锁操作。
    a、静态方法:Java类对应的Class类的对象所关联的监视器对象。
    b、实例方法:当前对象实例所关联的监视器对象。
    c、代码块:代码块声明中的对象所关联的监视器对象。
    注:当锁被释放,对共享变量的修改会写入主存;当活得锁,CPU缓存中的内容被置为无效。编译器在处理synchronized方法或代码块,不会把其中包含的代码移动到synchronized方法或代码块之外,从而避免了由于代码重排而造成的问题。
    例:以下方法getNext()和getNextV2() 都获得了当前实例所关联的监视器对象

    public class SynchronizedIdGenerator{
       private int value = 0;
       public synchronized int getNext(){
          return value++;
       }
       public int getNextV2(){
          synchronized(this){
             return value++;
          }
       }
    }

    2、Object类的wait、notify和notifyAll方法
    生产者和消费者模式,判断缓冲区是否满来消费,缓冲区是否空来生产的逻辑。如果用while 和 volatile也可以做,不过本质上会让线程处于忙等待,占用CPU时间,对性能造成影响。
    wait: 将当前线程放入,该对象的等待池中,线程A调用了B对象的wait()方法,线程A进入B对象的等待池,并且释放B的锁。(这里,线程A必须持有B的锁,所以调用的代码必须在synchronized修饰下,否则直接抛出java.lang.IllegalMonitorStateException异常)。
    notify:将该对象中等待池中的线程,随机选取一个放入对象的锁池,当当前线程结束后释放掉锁, 锁池中的线程即可竞争对象的锁来获得执行机会。
    notifyAll:将对象中等待池中的线程,全部放入锁池。
    (notify锁唤醒的线程选择由虚拟机实现来决定,不能保证一个对象锁关联的等待集合中的线程按照所期望的顺序被唤醒,很可能一个线程被唤醒之后,发现他所要求的条件并没有满足,而重新进入等待池。因为当等待池中包含多个线程时,一般使用notifyAll方法,不过该方法会导致线程在没有必要的情况下被唤醒,之后又马上进入等待池,对性能有影响,不过能保证程序的正确性)
    工作流程:
    a、Consumer线程A 来 看产品,发现产品为空,调用产品对象的wait(),线程A进入产品对象的等待池并释放产品的锁。
    b、Producer线程B获得产品的锁,执行产品的notifyAll(),Consumer线程A从产品的等待池进入锁池,Producer线程B生产产品,然后退出释放锁。
    c、Consumer线程A获得产品锁,进入执行,发现有产品,消费产品,然后退出。
    例子:

    public synchronized String pop(){
      this.notifyAll();// 唤醒对象等待池中的所有线程,可能唤醒的就是 生产者(当生产者发现产品满,就会进入对象的等待池,这里代码省略,基本略同)
       while(index == -1){//如果发现没产品,就释放锁,进入对象等待池
          this.wait();
       }//当生产者生产完后,消费者从this.wait()方法再开始执行,第一次还会执行循环,万一产品还是为空,则再等待,所以这里必须用while循环,不能用if
       String good = buffer[index];
       buffer[index] = null;
       index--;
       return good;// 消费完产品,退出。
    }

    注:wait()方法有超时和不超时之分,超时的在经过一段时间,线程还在对象的等待池中,那么线程也会推出等待状态。
    3、线程状态转换:
    已经废弃的方法:stop、suspend、resume、destroy,这些方法在实现上时不安全的。
    线程的状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING(有超时的等待)、TERMINATED。
    a、方法sleep()进入的阻塞状态,不会释放对象的锁(即大家一起睡,谁也别想执行代码),所以不要让sleep方法处在synchronized方法或代码块中,否则造成其他等待获取锁的线程长时间处于等待。
    b、方法join()则是主线程等待子线程完成,再往下执行。例如main方法新建两个线程A和B

    public static void main(String[] args) throws InterruptedException {  
    Thread t1 = new Thread(new ThreadTesterA());  
    Thread t2 = new Thread(new ThreadTesterB());  
    t1.start();  
    t1.join(); // 等t1执行完再往下执行
    t2.start();  
    t2.join(); // 在虚拟机执行中,这句可能被忽略
    }
    

    c、方法interrupt(),向被调用的对象线程发起中断请求。如线程A通过调用线程B的d的interrupt方法来发出中断请求,线程B来处理这个请求,当然也可以忽略,这不是必须的。Object类的wait()、Thread类的join()和sleep方法都会抛出受检异常java.lang.InterruptedException,通过interrupt方法中断该线程会导致线程离开等待状态。对于wait()调用来说,线程需要重新获取监视器对象上的锁之后才能抛出InterruptedException异常,并致以异常的处理逻辑。
    可以通过Thread类的isInterrupted方法来判断是否有中断请求发生,通常可以利用这个方法来判断是否退出线程(类似上面的volatitle修饰符的例子);
    Thread类还有个方法Interrupted(),该方法不但可以判断当前线程是否被中断,还会清楚线程内部的中断标记,如果返回true,即曾被请求中断,同时调用完后,清除中断标记。
    如果一个线程在某个对象的等待池,那么notify和interrupt 都可以使该线程从等待池中被移除。如果同时发生,那么看实际发生顺序。如果是notify先,那照常唤醒,没影响。如果是interrupt先,并且虚拟机选择让该线程中断,那么即使nofity,也会忽略该线程,而唤醒等待池中的另一个线程。
    e、yield(),尝试让出所占有的CPU资源,让其他线程获取运行机会,对操作系统上的调度器来说是一个信号,不一定立即切换线程。(在实际开发中,测试阶段频繁调用yeid方法使线程切换更频繁,从而让一些多线程相关的错误更容易暴露出来)。
    这里写图片描述
    四、非阻塞方式
    线程之间同步机制的核心是监视对象上的锁,竞争锁来获得执行代码的机会。当一个对象获取对象的锁,然后其他尝试获取锁的对象会处于等待状态,这种锁机制的实现方式很大程度限制了多线程程序的吞吐量和性能(线程阻塞),且会带来死锁(线程A有a对象锁,等着获取b对象锁,线程B有b对象锁,等待获取a对象锁)和优先级倒置(优先级低的线程获得锁,优先级高的只能等待对方释放锁)等问题。
    如果能不阻塞线程,又能保证多线程程序的正确性,就能有更好的性能。
    在程序中,对共享变量的使用一般遵循一定的模式,即读取、修改和写入三步组成。之前碰到的问题是,这三步执行中可能线程执行切换,造成非原子操作。锁机制是把这三步变成一个原子操作。
    目前CPU本身实现 将这三步 合起来 形成一个原子操作,无需线程锁机制干预,常见的指令是“比较和替换”(compare and swap,CAS),这个指令会先比较某个内存地址的当前值是不是指定的旧指,如果是,就用新值替换,否则什么也不做,指令返回的结果是内存地址的当前值。通过CAS指令可以实现不依赖锁机制的非阻塞算法。一般做法是把CAS指令的调用放在一个无限循环中,不断尝试,知道CAS指令成功完成修改。
    java.util.concurrent.atomic包中提供了CAS指令。(不是所有CPU都支持CAS,在某些平台,java.util.concurrent.atomic的实现仍然是锁机制)
    atomic包中提供的Java类分成三类:
    1、支持以原子操作来进行更新的数据类型的Java类(AtomicBoolean、AtomicInteger、AtomicReference),在内存模型相关的语义上,这四个类的对象类似于volatile变量。
    类中的常用方法:
    a、compareAndSet:接受两个参数,一个是期望的旧值,一个是替换的新值。
    b、weakCompareAndSet:效果同compareAndSet(JSR中表示weak原子方式读取和有条件地写入变量但不创建任何 happen-before 排序,但在源代码中和compareAndSet完全一样,所以并没有按JSR实现)
    c、get和set:分别用来直接获取和设置变量的值。
    d、lazySet:与set类似,但允许编译器把lazySet方法的调用与后面的指令进行重排,因此对值得设置操作有可能被推迟。
    例:

    public class AtomicIdGenerator{
       private final AtomicInter counter = new AtomicInteger(0);
       public int getNext(){
          return counter.getAndIncrement();
       }
    }
    // getAndIncrement方法的内部实现方式,这也是CAS方法的一般模式,CAS方法不一定成功,所以包装在一个无限循环中,直到成功
    public final int getAndIncrement(){
       for(;;){
          int current = get();
          int next = current +1;
          if(compareAndSet(current,next))
             return current;
       }
    }

    2、提供对数组类型的变量进行处理的Java类,AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray类。(同上,只是放在类数组里,调用时也只是多了一个操作元素索引的参数)
    3、通过反射的方式对任何对象中包含的volatitle变量使用CAS方法,AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater。他们提供了一种方式把CAS的功能扩展到了任何Java类中声明为volatitle的域上。(灵活,但语义较弱,因为对象的volatitle可能被非atomic的其他方式被修改)

    public class TreeNode{
       private volatile TreeNode parent;
    // 静态工厂方法
       private static final AtomicReferenceFieldUpdater<TreeNode, TreeNode> parentUpdater = AtomicReferenceFieldUpdater.newUpdater(TreeNode.class,TreeNode.class,"parent");
    public boolean compareAndSetParent(TreeNode expect, TreeNode update){
          return parentUpdater.compareAndSet(this, expect, update);
    }
    }

    注:java.util.concurrent.atomic包中的Java类属于比较底层的实现,一般作为java.util.concurrent包中很多非阻塞的数据结构的实现基础。
    比较多的用AtomicBoolean、AtomicInteger、AtomicLong和AtomicReference。在实现线程安全的计数器时,AtomicInteger和AtomicLong类时最佳的选择。
    五、高级同步机制(比synchronized更灵活的加锁机制)
    synchronized和volatile,以及wait、notify等方法抽象层次低,在程序开发中使用比较繁琐,易出错。
    而多线程之间的交互来说,存在某些固定的模式,如生产者-消费者和读者-写者模式,把这些模式抽象成高层API,使用起来会非常方便。
    java.util.concurrent包为多线程提供了高层的API,满足日常开发中的常见需求。
    常用接口
    1、Lock接口,表示一个锁方法:
    a、lock(),获取所,如果无法获取所锁,会处于等待状态
    b、unlock(),释放锁。(一般放在finally代码块中)
    c、lockInterruptibly(),与lock()类似,但允许当前线程在等待获取锁的过程中被中断。(所以要处理InterruptedException)
    d、tryLock(),以非阻塞方式获取锁,如果无法获取锁,则返回false。(tryLock()的另一个重载可以指定超时,如果指定超时,当无法获取锁,会等待而阻塞,同时线程可以被中断)
    2、ReadWriteLock接口,表示两个锁,读取的共享锁和写入的排他锁。(适合常见的读者–写者场景)
    ReadWriteLock接口的readLock和writeLock方法来获取对应的锁的Lock接口的实现。
    在多数线程读取,少数线程写入的情况下,可以提高多线程的性能,提高使用该数据结构的吞吐量。
    如果是相反的情况,较多的线程写入,则接口会降低性能。
    3、ReentrantLock类和ReentrantReadWriteLock,分别为上面两个接口的实现类。
    他们具有重入性:即允许一个线程多次获取同一个锁(他们会记住上次获取锁并且未释放的线程对象,和加锁的次数,getHoldCount())
    同一个线程每次获取锁,加锁数+1,每次释放锁,加锁数-1,到0,则该锁被释放,可以被其他线程获取。

    public class LockIdGenrator{
    //new ReentrantLock(true)是重载,使用更加公平的加锁机制,在锁被释放后,会优先给等待时间最长的线程,避免一些线程长期无法获得锁
       private int ReentrantLock lock = ReentrantLock();
       privafte int value = 0;
       public int getNext(){
          lock.lock();      //进来就加锁,没有锁会等待
          try{
             return value++;//实际操作
          }finally{
             lock.unlock();//释放锁
          }
       }
    }

    注:重入性减少了锁在各个线程之间的等待,例如便利一个HashMap,每次next()之前加锁,之后释放,可以保证一个线程一口气完成便利,而不会每次next()之后释放锁,然后和其他线程竞争,降低了加锁的代价, 提供了程序整体的吞吐量。(即,让一个线程一口气完成任务,再把锁传递给其他线程)。
    4、Condition接口,Lock接口代替了synchronized,Condition接口替代了object的wait、nofity。
    a、await(),使当前线程进入等待状态,知道被唤醒或中断。重载形式可以指定超时时间。
    b、awaitNanos(),以纳秒为单位等待。
    c、awaitUntil(),指定超时发生的时间点,而不是经过的时间,参数为java.util.Date。
    d、awaitUninterruptibly(),前面几种会响应其他线程发出的中断请求,他会无视,直到被唤醒。
    注:与Object类的wait()相同,await()会释放其所持有的锁。
    e、signal()和signalAll, 相当于 notify和notifyAll

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    lock.lock();
    try{
       while(/*逻辑条件不满足*/){
          condition.await();   
       }
    }finally{
       lock.unlock();
    }
    

    六、底层同步器
    多线程程序中,线程之间存在多种不同的同步方式。除了Java标准库提供的同步方式之外,程序中特有的同步方式需要由开发人员自己来实现。
    常见的一种需求是 对有限个共享资源的访问,比如多台个人电脑,2台打印机,当多个线程在等待同一个资源时,从公平角度出发,会用FIFO队列。
    如果程序中的同步方式可以抽象成对有限个资源的访问,那么可以使用java.util.concurrent.locks包中的AbstractQueuedSynchronizer类和AbstractQueuedLongSynchronizer类作为实现的基础,前者用int类型的变量来维护内部状态,而后者用long类型。(可以将这个变量理解为共享资源个数)
    通过getState、setState、和compareAndSetState3个方法更新内部变量的值。
    AbstractQueuedSynchronizer类是abstract的,需要覆盖其中包含的部分方法,通常做法是把其作为一个Java类的内部类,外部类提供具体的同步方式,内部类则作为实现的基础。有两种模式,排他模式和共享模式,分别对应方法 tryAcquire()、tryRelease 和 tryAcquireShared、tryReleaseShared,在这些方法中,使用getState、setState、compareAndSetState3个方法来修改内部变量的值,以此来反应资源的状态。

    public class SimpleResourceManager{
       private final InnerSynchronizer synchronizer;
       private static class InnerSynchronizer extends AbstractQueuedSynchronizer{
          InnerSynchronizer(int numOfResources){
             setState(numOfResources);
          }
          protected int tryAcquireShared(int acquires){
             for(;;){
                int available = getState();
                int remain = available - acquires;
                if(remain <0 || comapreAndSetState(available, remain){
                   return remain;
                }
             }
          }
          protected boolean try ReleaseShared(int releases){
             for(;;){
                int available = getState(); 
                int next = available + releases; 
                if(compareAndSetState(available,next){
                   return true;
                }
             }
          }
       }
       public SimpleResourceManager(int numOfResources){
          synchronizer = new InnerSynchronizer(numOfResources);
       }
       public void acquire() throws InterruptedException{
          synchronizer.acquireSharedInterruptibly(1);
       }      
       pubic void release(){    
          synchronizer.releaseShared(1);
        }
    }
    

    七、高级同步对象(提高开发效率)
    atomic和locks包提供的Java类可以满足基本的互斥和同步访问的需求,但这些Java类的抽象层次较低,使用比较复杂。
    更简单的做法是使用java.util.concurrent包中的高级同步对象。
    1、信号量。
    信号量一般用来数量有限的资源,每类资源有一个对象的信号量,信号量的值表示资源的可用数量。
    在使用资源时,需要从该信号量上获取许可,成功获取许可,资源的可用数-1;完成对资源的使用,释放许可,资源可用数+1; 当资源数为0时,需要获取资源的线程以阻塞的方式来等待资源,或过段时间之后再来检查资源是否可用。(上面的SimpleResourceManager类实际上时信号量的一个简单实现)
    java.util.concurrent.Semaphore类,在创建Semaphore类的对象时指定资源的可用数
    a、acquire(),以阻塞方式获取许可
    b、tryAcquire(),以非阻塞方式获取许可
    c、release(),释放许可。
    d、accquireUninterruptibly(),accquire()方法获取许可以的过程可以被中断,如果不希望被中断,使用此方法。

    public class PrinterManager{
       private final Semphore semaphore;
       private final List<Printer> printers = new ArrayList<>():
       public PrinterManager(Collection<? extends Printer> printers){
          this.printers.addAll(printers);
          //这里重载方法,第二个参数为true,以公平竞争模式,防止线程饥饿
          this.semaphore = new Semaphore(this.printers.size(),true);
       }
       public Printer acquirePrinter() throws InterruptedException{
          semaphore.acquire();
          return getAvailablePrinter();
       }
       public void releasePrinter(Printer printer){
          putBackPrinter(pinter);
          semaphore.release();
       }
       private synchronized Printer getAvailablePrinter(){
          printer result = printers.get(0);
          printers.remove(0);
          return result;
       }
       private synchronized void putBackPrinter(Printer printer){
          printers.add(printer);
       }
    }

    2、倒数闸门
    多线程协作时,一个线程等待另外的线程完成任务才能继续进行。
    java.util.concurrent.CountDownLatch类,创建该类时,指定等待完成的任务数;当一个任务完成,调用countDonw(),任务数-1。等待任务完成的线程通过await(),进入阻塞状态,直到任务数量为0。CountDownLatch类为一次性,一旦任务数为0,再调用await()不再阻塞当前线程,直接返回。
    例:

    public class PageSizeSorter{
       // 并发性能远远优于HashTable的 Map实现,hashTable做任何操作都需要获得锁,同一时间只有有个线程能使用,而ConcurrentHashMap是分段加锁,不同线程访问不同的数据段,完全不受影响,忘记HashTable吧。
       private static final ConcurrentHashMap<String , Interger> sizeMap = new ConcurrentHashMap<>();
       private static class GetSizeWorker implements Runnable{
          private final String urlString;
          public GetSizeWorker(String urlString , CountDownLatch signal){
             this.urlString = urlStirng;
             this.signal = signal;
          }
          public void run(){
             try{
                InputStream is = new URL(urlString).openStream();
                int size = IOUtils.toByteArray(is).length;
                sizeMap.put(urlString, size);
             }catch(IOException e){
                sizeMap.put(urlString, -1);
             }finally{
                signal.countDown()://完成一个任务 , 任务数-1
             }
          }
       }
       private void sort(){
          List<Entry<String, Integer> list = new ArrayList<sizeMap.entrySet());
          Collections.slort(list, new Comparator<Entry<String,Integer>>(){
             public int compare (Entry<String, Integer> o1, Entry<Sting , Integer> o2){
                return Integer.compare(o2.getValue(),o1.getValue());
          };
          System.out.println(Arrays.deepToString(list.toArray()));
       }
       public void sortPageSize(Collection<String> urls) throws InterruptedException{
          CountDownLatch sortSignal = new CountDownLatch(urls.size());
          for(String url: urls){
             new Thread(new GetSizeWorker(url, sortSignal)).start();
          }
          sortSignal.await()://主线程在这里等待,任务数归0,则继续执行
          sort();
       }
    }
    

    3、循环屏障
    循环屏障在作用上类似倒数闸门,不过他不像倒数闸门是一次性的,可以循环使用。另外,线程之间是互相平等的,彼此都需要等待对方完成,当一个线程完成自己的任务之后,等待其他线程完成。当所有线程都完成任务之后,所有线程才可以继续运行。
    当线程之间需要再次进行互相等待时,可以复用同一个循环屏障。
    类java.uti.concurrent.CyclicBarrier用来表示循环屏障,创建时指定使用该对象的线程数目,还可以指定一个Runnable接口的对象作为每次循环后执行的动作。(当最后一个线程完成任务之后,所有线程继续执行之前,被执行。如果线程之间需要更新一些共享的内部状态,可以利用这个Runnalbe接口的对象来处理)。
    每个线程任务完成之后,通过调用await方法进行等待,当所有线程都调用await方法之后,处于等待状态的线程都可以继续执行。在所有线程中,只要有一个在等待中被中断,超时或是其他错误,整个循环屏障会失败,所有等待中的其他线程抛出java.uti.concurrent.BrokenBarrierException。
    例:每个线程负责找一个数字区间的质数,当所有线程完成后,如果质数数目不够,继续扩大范围查找

    public class PrimeNumber{
       private static final int TOTAL_COUTN = 5000;
       private static final int RANGE_LENGTH= 200;
       private static final int WORKER_NUMBER = 5;
       private static volatitle boolean done = false;
       private static int rangeCount = 0;
       private static final List<Long> results = new ArrayList<Long>():
       private static final CyclicBarrier barrier = new CyclicBarrier(WORKER_NUMBER, new Runnable(){
          public void run(){
             if(results.size() >= TOTAL_COUNT){
                done = true;
             }
         }
       });
       private static class PrimeFinder implements Runnable{
          public void run(){
             while(!done){// 整个过程在一个 while循环下,await()等待,下次循环开始,会再次判断 执行条件
                int range = getNextRange();
                long start = rang * RANGE_LENGTH;
                long end = (range + 1) * RANGE_LENGTH;
                for(long i = start; i<end;i++){
                   if(isPrime(i)){
                      updateResult(i);
                   }
                }
                try{
                   barrier.await();
                }catch (InterruptedException | BokenBarrierException e){
                   done =  true;
                }
             }
          }
       }
       private synchronized static void updateResult(long value){
          results.add(value);
       }
       private synchronized static int getNextRange(){
          return rangeCount++;
       }
       private static boolean isPrime(long number){
          //找质数的代码
       }
       public void calculate(){
          for(int i=0;i<WORKER_NUMBER;i++){
             new Thread(new PrimeFinder()).start();
          }
          while(!done){
    
          }
          //计算完成
       }
    }

    4、对象交换器
    适合于两个线程需要进行数据交换的场景。(一个线程完成后,把结果交给另一个线程继续处理)
    java.util.concurrent.Exchanger类,提供了这种对象交换能力,两个线程共享一个Exchanger类的对象,一个线程完成对数据的处理之后,调用Exchanger类的exchange()方法把处理之后的数据作为参数发送给另外一个线程。而exchange方法的返回结果是另外一个线程锁提供的相同类型的对象。如果另外一个线程未完成对数据的处理,那么exchange()会使当前线程进入等待状态,直到另外一个线程也调用了exchange方法来进行数据交换。
    例:

    public class SendAndReceiver{
       private final Exchanger<StringBuilder> exchanger = new Exchanger<StringBuilder>();
       private class Sender implements Runnable{
          public void run(){
             try{
                StringBuilder content = new StringBuilder("Hello");
                content = exchanger.exchange(content);
             }catch(InterruptedException e){
                Thread.currentThread().interrupt();
             }
          }
       }
       private class Receiver implements Runnable{
          public void run(){
             try{
                StringBuilder content = new StringBuilder("World");
                content = exchanger.exchange(content);
             }catch(InterruptedException e){
                Thread.currentThread().interrupt();
             }
          }
       }
       public void exchange(){
          new Thread(new Sender()).start();
          new Thread(new Receiver()).start();
       }
    }

    八、数据结构(多线程程序使用的高性能数据结构)
    java.util.concurrent包中提供了一些适合多线程程序使用的高性能数据结构,包括队列和集合类对象等。
    1、队列
    a、BlockingQueue接口:线程安全的阻塞式队列;当队列已满时,想队列添加会阻塞;当队列空时,取数据会阻塞。(非常适合消费者-生产者模式)
    阻塞方式:put()、take()。
    非阻塞方式:offer()、poll()。
    实现类:基于数组的固定元素个数的ArrayBolockingQueue和基于链表结构的不固定元素个数的LinkedBlockQueue类。
    b、BlockingDeque接口: 与BlockingQueue相似,但可以对头尾进行添加和删除操作的双向队列;方法分为两类,分别在队首和对尾进行操作。
    实现类:标准库值提供了一个基于链表的实现,LinkedBlockgingDeque。
    2、集合类
    在多线程程序中,如果共享变量时集合类的对象,则不适合直接使用java.util包中的集合类。这些类要么不是线程安全,要么在多线程下性能比较差。
    应该使用java.util.concurrent包中的集合类。
    a、ConcurrentMap接口: 继承自java.util.Map接口
    putIfAbsent():只有在散列表不包含给定键时,才会把给定的值放入。
    remove():删除条目。
    replace(key,value):把value 替换到给定的key上。
    replace(key, oldvalue, newvalue):CAS的实现。
    实现类:ConcurrentHashMap:
    创建时,如果可以预估可能包含的条目个数,可以优化性能。(因为动态调整所能包含的数目操作比较耗时,这个HashMap也一样,只是多线程下更耗时)。
    创建时,预估进行更新操作的线程数,这样实现中会根据这个数把内部空间划分为对应数量的部分。(默认是16,如果只有一个线程进行写操作,其他都是读取,那么把值设为1 可以提高性能)。
    注:当从集合中创建出迭代器遍历Map元素时,不一定能看到正在添加的数据,只能和集合保证弱一致性。(当然使用迭代器不会因为查看正在改变的Map,而抛出java.util.ConcurrentModifycationException)
    b、CopyOnWriteArrayList接口:继承自java.util.List接口。
    顾名思义,在CopyOnWriteArrayList的实现类,所有对列表的更新操作都会新创建一个底层数组的副本,并使用副本来存储数据;对列表更新操作加锁,读取操作不加锁。
    适合多读取少修改的场景,如果更新操作多,那么不适合用,同样迭代器只能表示创建时列表的状态,更新后使用了新的底层数组,迭代器还是引用旧的底层数组。
    九、多线程任务的执行
    过去线程的执行,是先创建Thread类的想,再调用start方法启动,这种做法要求开发人员对线程进行维护,在线程较多时,一般创建一个线程池同一管理,同时降低重复创建线程的开销
    在J2SE5.0中,java.util.concurrent包提供了丰富的用来管理线程和执行任务的实现。
    1、基本接口(描述任务)
    a、Callable接口:
    Runnable接口受限于run方法的类型签名,而Callable只有一个方法call(),可以有返回值,可以抛出受检异常。
    b、Future接口:
    过去,需要异步线程的任务执行结果,要求主线程和任务执行线程之间进行同步和数据传递。
    Future简化了任务的异步执行,作为异步操作的一个抽象。调用get()方法可以获取异步的执行结果,如果任务没有执行完,会等待,直到任务完成或被取消,cancel()可以取消。
    c、Delayed接口:
    延迟执行任务,getDelay()返回当前剩余的延迟时间,如果不大于0,说明延迟时间已经过去,应该调度并执行该任务。
    2、组合接口(描述任务)
    a、RunnableFuture接口:继承自Runnable接口和Future接口。
    当来自Runnalbe接口中的run方法成功执行之后,相当于Future接口表示的异步任务已经完成,可以通过get()获取运行结果。
    b、ScheduledFuture接口:继承Future接口和Delayed接口,表示一个可以调用的异步操作。
    c、RunnableScheduledFuture接口:继承自Runnable、Delayed和Future,接口中包含isPeriodic,表明该异步操作是否可以被重复执行。
    3、Executor接口、ExcutorServer接口、ScheduleExecutorService接口和CompletionService接口(描述任务执行)
    a、executor接口,execute()用来执行一个Runnable接口的实现对象,不同的Executor实现采取不同执行策略,但提供的任务执行功能比较弱。
    b、excutorServer接口,继承自executor;
    提供了对任务的管理:submit(),可以吧Callable和Runnable作为任务提交,得到一个Future作为返回,可以获取任务结果或取消任务。
    提供批量执行:invokeAll()和invokeAny(),同时提交多个Callable;invokeAll(),会等待所有任务都执行完成,返回一个包含每个任务对应Future的列表;invokeAny(),任何一个任务成功完成,即返回该任务结果。
    提供任务关闭:shutdown()、shutdownNow()来关闭服务,前者不允许新的任务提交,后者试图终止正在运行和等待的任务,并返回已经提交单没有被运行的任务列表。(两个方法都不会等待服务真正关闭,只是发出关闭请求。)。shutdownDow,通常做法是向线程发出中断请求,所以确保提交的任务实现了正确的中断处理逻辑。
    c、ScheduleExecutorService接口,继承自excutorServer接口:支持任务的延迟执行和定期执行,可以执行Callable或Runnable。
    schedule(),调度一个任务在延迟若干时间之后执行;
    scheduleAtFixedRate():在初始延迟后,每隔一段时间循环执行;在下一次执行开始时,上一次执行可能还未结束。(同一时间,可能有多个)
    scheduleWithFixedDelay:同上,只是在上一次任务执行完后,经过给定的间隔时间再开始下一次执行。(同一时间,只有一个)
    以上三个方法都返回ScheduledFuture接口的实现对象。
    d、CompletionService接口,共享任务执行结果。
    通常在使用ExecutorService接口,通过submit提交任务,并得到一个Future接口来获取任务结果,如果任务提交者和执行结果的使用者是程序的不同部分,那就要把Future在不同部分进行传递;而CompletionService就是解决这个问题,程序不同部分可以共享CompletionService,任务提交后,执行结果可以通过take(阻塞),poll(非阻塞)来获取。
    标准库提供的实现是 ExecutorCompletionService,在创建时,需要提供一个Executor接口的实现作为参数,用来实际执行任务。
    例:多线程方式下载文件

    public class FileDownloader{
       // 线程池
       private final ExecutorService executor = Executors.newFixedThreadPool(10);
       public boolean download(final URL url, final Path path){
       Future<Path> future = executor.submit(new Callable<Path>(){ //submit提交任务
          public Path call(){
             //这里就省略IOException的处理了
             InputStream is = url.openStream();
             Files.copy(is, path, StandardCopyOption.REPLACE_EXISTING);
             return path;
          });
          try{
             return future.get() !=null ? true : false;
          }<span style="font-family: Arial, Helvetica, sans-serif;">catch(InterruptedException | ExecutionException e){</span>
                 return false;
          }
       }
       public void close(){//当不再使用FileDownloader类的对象时,应该使用close方法关闭其中包含的ExecutorService接口的实现对象,否则虚拟机不会退出,占用内存不释放
          executor.shutdown();// 发出关闭请求,此时不会再接受新任务
          try{
             if(!executor.awaitTermination(3, TimeUnit.MINUTES)){// awaitTermination 来等待一段时间,使正在执行的任务或等待的任务有机会完成
                executor.shutdownNow();// 如果等待时间过后还有任务没完成,则强制结束
                executor.awaitTermination(1, TimeUnit.MINUTES);// 再等待一段时间,使被强制结束的任务完成必要的清理工作
             }
          }catch(InterruptedException e){
             executor.shutdownNow();
             Thread.currentThread().interrupt();
          }
       }
    }

    十、Java SE 7 新特性
    对java.util.concurrent包进行更新,增加了新的轻量级任务执行框架fork/join和多阶段线程同步工具。
    1、轻量级任务执行框架fork/join
    这个框架的目的主要是更好地利用底层平台上的多核和多处理器来进行并行处理。
    通过分治算法或map/reduce算法来解决问题。
    fork/join 类比于 map/reduce。
    fork操作是把一个大的问题划分为若干个较小的问题,划分过程一般为递归,直到可以直接进行计算的粒度适合的子问题;子问题在结算后,可以得到整个问题的部分解
    join操作收集子结果,合并,得到完整解,也可能是 递归进行的。
    相对一般的线程池实现,F/J框架的优势在任务的处理方式上。在一般线程池中,一个线程由于某些原因无法运行,会等待;而在F/J,某个子问题由于等待另外一个子问题的完成而无法继续运行,那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行。这种方式减少了等待时间,提高了性能。
    为了F/J能高效,在每个子问题视线中应避免使用synchronized或其他方式进行同步,也不应使用阻塞式IO或过多访问共享变量。在理想情况下,每个子问题都应值进行CPU计算,只使用每个问题的内部对象,唯一的同步应只发生在子问题和创建它的父问题之间。(这完全就是Hadoop的MapReduce嘛)
    a、ForkJoinTask类:表示一个由F/J框架执行的任务,该类实现了Future接口,可以按照Future接口的方式来使用。(表示任务)
    fork(),异步方式启动任务的执行。
    join(),等待任务完成并返回执行结果。
    在创建自己的任务时,最好不要直接继承自ForkJoinTask,而是继承其子类,RecuriveTask或RecursiveAction,前者可以返回结果,后者不行。
    b、ForkJoinPool类:表示任务执行,实现了ExecutorService接口,除了可以执行ForkJoinTask,也可以执行Callable和Runnable。(任务执行)
    执行任务的两大类:
    第一类:execute、invoke或submit方法:直接提交任务。
    第二类:fork():运行ForkJoinTask在执行过程中的子任务。
    一般作法是表示整个问题的ForkJoinTask用第一类提交,执行过程中产生的子任务不需要处理,ForkJoinPool会负责子任务执行。
    例:查找数组中的最大值

    private static class MaxValueTask extends RecursiveTask<Long>{
       private final long[] array;
       private final int start;
       private final int end;
       MaxValueTask(long[] array, int start, int end){
          this.array = array;
          this.start = start;
          this.end = end;
       }
       //compute是RecursiveTask的主方法
       protected long compute(){
          long max = Long.MIN_VALUE;
          if(end - start < RANG_LENGTH){//寻找最大值
             for(int i = start; i<end;i++{
                if(array[i] > max){
                   max = array[i];
                }
             }
          }else{// 二分任务
             int mid = (start + end) /2;
             MaxValueTask lowTask = new MaxValueTask(array, start , mid);
             MaxValueTask highTask = new MaxValueTask(array, mid, end);
             lowTask.fork();// 异步启动任务
             highTask.fork();
             max = Math.max(max, lowTask.join());//等待执行结果
             max = Math.max(max, highTask.join();
          }
          return max;
       }
       public Long calculate(long[] array){
          MaxValueTask task = new MaxValueTask(array, 0 , array.length);
          Long result = forkJoinPool.invoke(task);
          return result;
       }
    }

    注:这个例子是示例,但从性能上说直接对整个数组顺序比较效率高,毕竟多线程所带来的额外开销过大。
    在实际中,F/J框架发挥作用的场合很多,比如在一个目录包含的所有文本中搜索某个关键字,可以每个文件创建一个子任务。
    如果相关的功能可以用递归和分治来解决,就适合F/J。
    2、多阶段线程同步工具
    Phaser类是Java SE 7中新增的一个使用同步工具,功能和灵活性比倒数闸门和循环屏障要强很多。
    在F/J框架中的子任务之间要进行同步时,应优先考虑Phaser。
    Phaser把多个线程写作执行的任务划分成多个阶段(phase),编程时要明确各个阶段的任务,每个阶段都可以有任意个参与者,线程可以随时注册并参与到某个阶段,当一个阶段中所有线程都成功完成之后,Phaser的onAdvance()被调用,可以通过覆盖添加自定义处理逻辑(类似循环屏障的使用的Runnable接口),然后Phaser类会自动进入下个阶段。如此循环,知道Phaser不再包含任何参与者。
    Phaser创建后,初始阶段编号为0,构造函数中指定初始参与个数。
    register(),bulkRegister(),动态添加一个或多个参与者。
    arrive(),某个参与者完成任务后调用
    arriveAndDeregister(),任务完成,取消自己的注册。
    arriveAndAwaitAdvance(),自己完成等待其他参与者完成。,进入阻塞,直到Phaser成功进入下个阶段。
    awaitAdvance()、awaitAdvanceInterruptibly(),等待phaser进入下个阶段,参数为当前阶段的编号,后者可以设置超时和处理中断请求。
    另外,Phaser的一个重要特征是多个Phaser可以组成树形结构,Phaser提供了构造方法来指定当前对象的父对象;当一个子对象参与者>0,会自动注册到父对象中;当=0,自动解除注册。
    例:从指定网址,下载img标签的照片
    阶段1、处理网址对应的html文本,和抽取img的链接;2、创建图片下载子线程,主线程等待;3、子线程下载图片,主线程等待;4、任务完成退出

    public class WebPageImageDownloader{
       private final Phaser phaser = new Phaser(1);//初始参与数1,代表主线程。
       public void download(URL url, final Path path) throws IOException{
          String content = getContent(url);//获得HTML文本,省略。
          List<URL> imageUrls = extractImageUrls(content);//获得图片链接,省略。
          for(final URL imageUrl : imageUrls){
             phaser.register();//子线程注册
             new Thread(){
                public void run(){
                   phaser.arriveAndAwaitAdvance();//第二阶段的等待,等待进入第三阶段
                   try{
                      InputStream is = imageUrl.openStream();
                      File.copy(is, getSavePath(path, imageUrl), StandardCopyOption.REPLACE_EXISTING);
                   }catch(IOException e){
                      e.printStackTrace():
                   }finally{
                      phaser.arriveAndDeregister();//子线程完成任务,退出。
                   }
                }
            }.start();
          }
          phaser.arriveAndAwaitAdvance();//第二阶段等待,子线程在注册
          phaser.arriveAndAwaitAdvance();//第三阶段等待,子线程在下载
          phaser.arriveAndDeregister();//所有线程退出。
       }
    }

    十一、ThreadLocal类
    java.lang.ThreadLocal,线程局部变量,把一个共享变量变为一个线程的私有对象。不同线程访问一个ThreadLocal类的对象时,锁访问和修改的事每个线程变量各自独立的对象。通过ThreadLocal可以快速把一个非线程安全的对象转换成线程安全的对象。(同时也就不能达到数据传递的作用了)。
    a、get()和set()分别用来获取和设置当前线程中包含的对象的值。
    b、remove(),删除。
    c、initialValue(),初始化值。如果没有通过set方法设置值,第一个调用get,会通过initValue来获取对象的初始值。
    ThreadLoacl的一般用法,创建一个ThreadLocal的匿名子类并覆盖initalValue(),把ThreadLoacl的使用封装在另一个类中

    public class ThreadLocalIdGenerator{
       private static final ThreadLocal<IdGenerator> idGenerator = new ThreadLocal<IdGenerator>(){
             protected IdGenerator initalValue(){
                return new IdGenerator();//IdGenerator 是个初始int value =0,然后getNext(){  return value++}
             }
          };
       public static int getNext(){
          return idGenerator.get().getNext();
       }
    }

    ThreadLoal的另外一个作用是创建线程唯一的对象,在有些情况,一个对象在代码中各个部分都需要用到,传统做法是把这个对象作为参数在代码间传递,如果使用这个对I昂的代码都在同一个线程,可以封装在ThreadLocal中。
    如:在多线程中,生成随机数
    java.util.Random会带来竞争问题,java.util.concurrent.ThreadLocalRandom类提供多线程下的随机数声场,底层是ThreadLoacl。

    总结:多线程开发中应该优先使用高层API,如果无法满足,使用java.util.concurrent.atomic和java.util.concurrent.locks包提供的中层API,而synchronized和volatile,以及wait,notify和notifyAll等低层API 应该最后考虑。

    展开全文
  • Java多线程并发编程-线程池

    千次阅读 2019-04-14 00:04:34
    Java多线程并发编程线程池问题思考线程池原理任务用什么表示仓库用什么:BlockingQueue自己实现一个线程池JDK线程池API 线程池 问题思考 问题1、用多线程的目的是什么? 充分利用 CPU 资源,并发做多件事。 ...

    线程池

    问题思考

    • 问题1、用多线程的目的是什么?

    充分利用 CPU 资源,并发做多件事。

    • 问题2、单核 CPU 机器上适不适合用多线程?

    适合,如果是单线程,线程中需要等待 IO 时,此时 CPU 就空闲出来了。

    • 问题3、线程什么时候会让出 CPU?

    阻塞时,wait,await,等待IO,sleep,yield,执行结束了。。

    • 问题4、线程是什么?

    执行任务的基本单位。一条代码执行流,完成一组代码的执行。这一组代码,我们往往称为一个任务。

    • 问题5、CPU 做的是什么工作?

    执行代码
    在这里插入图片描述

    • 问题6、线程是不是越多越好?

    造卡车(线程)要不要时间?一次使用,用完了得销毁,销毁要不要耗时间?
    1、线程在java中是一个对象,每一个java线程都需要有一个操作系统线程支持。线程创建、销毁需要时间。如果 创建时间+销毁时间 > 执行任务时间 就很不合算。

    造很多的卡车,得需要空间来放它们,会不会造成内存紧张?
    2、java对象占用堆内存,操作系统线程占用系统内存,根据 jvm 规范,一个线程默认最大栈大小 1M,这个栈空间是需要从系统内存中分配的。
    线程过多,会消耗很多内存
    3、操作系统需要频繁切换线程上下文(大家都想被运行),影响性能。

    • 问题7、该如何正确使用多线程

    多线程目的:充分利用 CPU 并发做事
    线程的本质:将代码送给 CPU 执行
    用合适数量的卡车不断运送代码即可,这个合适数量的线程就构成了一个池。
    有任务要执行,就放入池中,池中的一个线程将把任务运送到 CPU 执行。

    线程池原理

    在这里插入图片描述

    • 接收任务,放入仓库
    • 工作线程从仓库取任务,执行
    • 当没有任务时,线程阻塞,当有任务时唤醒线程执行

    任务用什么表示

    • Runnable
    • Callable

    仓库用什么:BlockingQueue

    • 阻塞队列,线程安全的:在队列为空时获取阻塞,在队列满时放入阻塞。
    • BlockingQueue的方法以四种形式出现,对于不能立即满足但可能在将来某一时刻可以满足的操作,这四种形式的处理方式不同:第一种是抛出异常,第二种是返回一个特殊值(null或false,具体取决于操作),第三种是在操作可以成功前,无限期地阻塞当前线程,第四种是在放弃前只在给定的最大时间限制内阻塞。总结如下表:
    方法/处理方法 抛出异常 返回特殊值 一直阻塞 超时退出
    插入方法 add(e) offer(e) put(e) offer(e, time, unit)
    移出方法 remove() poll() take() poll(time, unit)
    检查方法 element() peek() 不可用 不可用
    • 问题8、如何确定合适数量的线程

    如果是计算型任务?
    CPU数量的1-2倍

    如果是IO型任务?
    需啊哟多一些线程,要根据具体IO阻塞时长进行考量决定。如 tomcat 中默认的最大线程数为200。也可以考虑根据需要在一个最小数量和最大数量间自动增减线程数。

    自己实现一个线程池

    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import java.util.concurrent.BlockingQueue;
    import java.util.concurrent.LinkedBlockingQueue;
    
    import static java.lang.System.out;
    
    /**
     * @description: 自己实现一个线程池
     * @author: 10217629
     * @date: 2019/4/12
     */
    public class FixedSizeThreadPool {
        // 思考:如果我们需要手写一个线程池,需要什么东西?
    
        // 1. 需要一个仓库
        private BlockingQueue<Runnable> blockingQueue;
    
        // 2. 需要一个线程集合
        private List<Thread> workers;
    
        // 3. 需要一个干活的线程
        public static class Worker extends Thread {
            private FixedSizeThreadPool pool;
    
            public Worker(FixedSizeThreadPool pool) {
                this.pool = pool;
            }
    
            @Override
            public void run() {
                // 去仓库拿东西
                while (this.pool.isWorking || this.pool.blockingQueue.size() > 0) {
                    Runnable task = null;
                    try {
                        if (this.pool.isWorking) {
                            // 阻塞方式
                            task = this.pool.blockingQueue.take();
                        } else {
                            // 非阻塞方式
                            task = this.pool.blockingQueue.poll();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    if (task != null) {
                        task.run();
                        out.println("线程:" + Thread.currentThread().getName() + "执行完毕");
                    }
                }
            }
        }
    
        // 4. 需要初始化规定好仓库的大小以及集合,同时把线程准备就绪
        public FixedSizeThreadPool(int poolSize, int taskSize) {
            if (poolSize <= 0 || taskSize <= 0) {
                throw new IllegalArgumentException("非法参数");
            }
            this.blockingQueue = new LinkedBlockingQueue<Runnable>(taskSize);
            // Collections.synchronizedList:处理线程安全
            this.workers = Collections.synchronizedList(new ArrayList<Thread>());
            for (int i = 0; i < poolSize; i++) {
                Worker worker = new Worker(this);
                worker.start();
                workers.add(worker);
            }
        }
    
        // 5. 需要向仓库放的代码(阻塞)
        public void excute(Runnable task) {
            try {
                this.blockingQueue.put(task);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        // 6. 需要向仓库放的代码(非阻塞)
        public boolean submit(Runnable task) {
            if (this.isWorking) {
                return this.blockingQueue.offer(task);
            } else {
                return false;
            }
        }
    
        // 7. 需要一个关闭方法
        // a. 关闭的时候,仓库要停止新的线程进来
        // b. 关闭的时候,如果仓库没有东西,我们要执行完
        // c. 关闭的时候,如果去仓库拿东西,我们就不能阻塞了
        // d. 关闭的时候,把阻塞的线程全部中断
        private volatile boolean isWorking = true;
        public void shutdown() {
            this.isWorking = false;
            for (Thread thread : workers) {
                if (thread.getState().equals(Thread.State.WAITING) || thread.getState().equals(Thread.State.BLOCKED)) {
                    thread.interrupt();
                }
            }
        }
    
        // 测试
        public static void main(String[] args) {
            FixedSizeThreadPool pool = new FixedSizeThreadPool(3, 6);
            for (int i = 0; i < 6; i++) {
                pool.submit(new Runnable() {
                    @Override
                    public void run() {
                        out.println("一个线程被放入到我们的仓库中...");
                        try {
                            Thread.sleep(2000L);
                        } catch (InterruptedException e) {
                            out.println("有线程被中断");
                        }
                    }
                });
            }
            pool.shutdown();
        }
    }
    

    JDK线程池API

    • Java并发包提供了丰富的线程池实现

    Executor

    • Executor 接口
      在这里插入图片描述

    ExecutorService

    • ExecutorService 加入了关闭方法和对Callable、Future的支持
      在这里插入图片描述

    ScheduledExecutorService

    • ScheduledExecutorService 加入对定时任务的支持
      在这里插入图片描述
      在这里插入图片描述

    Callable

    • Callable 对 Runnable 的改进:可以返回值,可以抛出异常
      在这里插入图片描述

    Future

    • Future 异步任务监视器,让提交者可以监控任务的执行
      在这里插入图片描述

    ThreadPoolExecutor

    • ThreadPoolExecutor 线程池标准实现
      在这里插入图片描述
      在这里插入图片描述

    Executors

    • Executors 创建线程池的工厂类,快速得到线程池的工具类,减轻我们的任务,它的工厂方法
      在这里插入图片描述
    • newFixedThreadPool(int nThreads)创建一个固定大小、任务队列容量无界的线程池。池的核心线程数 = 最大线程数 = nThreads。
    • newCachedThreadPool()创建的是一个大小无界的缓冲线程池。它的任务队列是一个同步队列。任务加入到池中,如果池中有空闲线程,则用空闲线程执行,如无则创建新线程执行。池中的线程空闲超过60秒,池中的核心线程数 = 0 最大线程数 = Integer.MAX_VALUE。
    • newSingleThreadExecutor()只有一个任务队列的单一线程池。该线程池确保任务按加入的顺序一个一个依次执行,任何时刻只有一个线程来执行无界队列的任务。当唯一的线程因任务异常终止时,将创建一个新的线程类继续执行后续的任务。单一线程与 newFixedThreadPool的区别在于:单一线程池的大小是不能再改变的。
    • newScheduledThreadPool(int corePoolSize):能定时执行任务的线程池。该池的核心线程数由参数指定,最大线程数 = Integer.MAX_VALUE。
    • newWorkStealingPool():以当前系统可用处理器数作为并行级别创建的 work-stealing thread pool。
    • newWorkStealingPool(int parallelism):以 parallelism 指定的并行级别创建的 work-stealing thread pool(ForkJoinPool)。
    展开全文
  • C++11的多线程并发编程(二) 疫情已到确诊3w+,望早日结束这疫情。 根据上一节学习的的线程概念,开始进一步学习C++11新标准的多线程编程,C++11引入了5个头文件,分别是:atomic, thread, mutex, condition_...

    C++11的多线程并发编程(二)

    疫情已到确诊3w+,望早日结束这疫情。
    根据上一节学习的的线程概念,开始进一步学习C++11新标准的多线程编程,C++11引入了5个头文件,分别是:atomic, thread, mutex, condition_variable, future。主要包含互斥类,条件变量,同步线程和异步线程等的相关类,这一节主要记录thread头文件的相关内容。

    1. 线程的创建std::thread
      std::thread的构造函数主要有三种:不带参数的构造函数,初始化构造函数,移动构造函数。写个代码举个栗子:
    #include <iostream>
    #include <thread>
    #include <unistd.h>
    
    using namespace std;
    void myFunc1()
    {	
    	cout << "hello comedy" << endl;
    }
    void myFunc2(int i)
    {
    	cout << "hello comedy" << i << endl;
    }
    int main(int argc, char *argv[])
    {
    	int i = 99;
    	thread t1(myFunc1);
    	thread t2(myFunc2,i);
    	thread t3(myFunc3,i);
    	thread t4(move(t3));
    
    	sleep(1);
    	cout << "the main thread is end;" << endl;
    	
    	return 0;
    }
    

    sleep(1)让主线程睡眠一秒钟,子线程有足够的时间去运行,因为进程是否执行完毕,取决于主线程是否执行结束。如主线程结束但子线程没有时间执行,则会被操作系统强制结束。

    1. 线程的阻塞 join()

    在thread类中提供了一个函数join();表示线程的加入和回合,其作用与sleep(n)等价,用来阻塞主线程,让主线程等到子线程执行完毕。用法如例:

    #include <iostream>
    #include <thread>
    
    using namespace std;
    
    void myFunc(char *str)
    {
    	cout << "hello " << str << endl;
    }
    
    int main(int argc, char *argv[])
    {
    	char str[] = "comedy!";
    	thread t1(myFunc , str);
    	t1.join();
    	cout << "the main thread is endl;" << endl;
    	return 0;
    }
    

    一个良好的程序是应该是主线程等待子线程执行完毕后,自己再结束。但thread类提供了一个成员函数detach()使得,子线程和主线程自己运行自己的,且无需等待。

    1. 线程分离detach()
      线程分离,子线程对象调用detach成员函数,使得子线程失去与主线程的关联,此时主线程结束而子线程未结束,子线程被后台C++运行库接管,分离线程即守护线程,或是缓存清理任务,或是监视文件任务。这里借鉴某个例子证明一个问题。
      在Ubuntu系统指定文件目录中编写c++程序文件。
      在这里插入图片描述
    #include <iostream>
    #include <thread>
    
    using namespace std;
    
    class myTest
    {
    	public:
    		int parameter;
    		myTest(int i): parameter(i){}
    		void operator()()
    		{
    			cout << "my thread is createed" << endl;
    			cout << "my parameter is :" << parameter << endl;
    			//do something.....
    			cout << "my thread is end" << endl;
    			
    		}
    };
    
    int main(int argc, char *argv[])
    {
    	int x = 99;
    	myTest test(x);
    	thread mythread(test);
    	mythread.detach();
    	
    	cout << "the main thread is end " << endl;
    	return 0;
    	
    }
    

    然后通过命令执行编译链接库 g++ 8.5.cpp -o 8.5 -pthread -std=c++11
    并通过命令执行 ./8.5 结果如图所示:
    在这里插入图片描述
    从图中可以看出,main线程很早就结束了,但是子线程还在执行,但是主线程的结束会这个test对象就会被销毁,那子线程的对象哪来的。通过修改案例代码:增加类的构造函数和拷贝构造函数和虚构函数。

    #include <iostream>
    #include <thread>
    
    using namespace std;
    
    class myTest
    {
    	public:
    		int parameter;
    		myTest(int i): parameter(i)
    		{
    			cout << "myTest's createFunction is running" << endl;
    		}
    		myTest(const myTest &test): parameter(test.parameter)
    		{
    			cout << "myTest's copyFunction is running" << endl;
    		}
    	
    		~myTest()
    		{
    			cout << "myTest's removeFunction is running" << endl;
    		}
    		void operator()()
    		{
    			cout << "my thread is createed" << endl;
    			cout << "my parameter is :" << parameter << endl;
    			//do something.....
    			cout << "my thread is end" << endl;
    			
    		}
    };
    
    int main(int argc, char *argv[])
    {
    	int x = 99;
    	myTest test(x);
    	thread mythread(test);
    	mythread.detach();
    	
    	cout << "the main thread is end " << endl;
    	return 0;
    	
    }
    

    运行结果如图所示:

    在这里插入图片描述
    结果显示第一个析构的对象实际上是被赋值进线程中的,执行完主线程主线程的对象被析构,当子线程执行结束时再调用析构函数。

    下一节继续学习线程传参部分。

    展开全文
  • 【动画详解原理系列】2.多线程并发编程与锁
  • 但是也只会基本的启动,了解基本的生命周期而已,并发这一块的东西基本还是不会,但是大部分公司都要求会并发,一般工作两三年的程序员也不一定搞得很清楚,所以自己开始尝试稍稍深入一下多线程并发编程,从多线程...
  • 多线程并发编程的基本问题

    千次阅读 多人点赞 2020-10-03 11:40:55
    事实上,多线程编程就不应该访问共享变量,如果真的要在多线程访问共享变量,唯一高效的方案就是 严格控制时序。 嗯,先来后到是唯一的方法。至于说设计这样那样的锁,那完全是惰政,只是为了防止出问题而已。 早在...
  • Java多线程并发编程之二volatile volatile:【线程安全包括:原子性+可见性】【“外练互斥,内修可见”】 1.多线程可以解决同步死循环问题;按顺序执行的两个函数,其中前个有死循环,后面一个修改循环条件...
  • 当多个线程要共享一个实例对象的值得时候,那么在考虑安全的多线程并发编程时就要保证下面3个要素:原子性(Synchronized, Lock)有序性(Volatile,Synchronized, Lock)可见性(Volatile,Synchronized,Lock)当然由于...
  • 对JAVA多线程 并发编程的理解

    千次阅读 2015-03-22 00:29:40
    对JAVA多线程并发编程的理解 Java多线程编程关注的焦点主要是对单一资源的并发访问,本文从Java如何实现支持并发访问的角度,浅析对并发编程的理解,也算是对前段时间所学的一个总结。 线程状态转换 Java语言定义...
  • 1.java多线程实现的几种方式: ...2.给一个demo样例主要根据多线程 Future模式角度来讲解多线程并发编程: 至于为什么选择第3种实现方式FutureTask,当然是因为这种方式可以有返回结果啊。上述...
  • Java多线程并发编程

    2019-03-12 19:18:05
    线程池是一种多线程的处理方式,利用已有线程对象继续服务新的任务(按照一定的执行策略),而不是频繁地创建销毁线程对象,由此提高服务的吞吐能力,减少CPU的闲置时间。具体组成部分包括: (1)、线程池管理器...
  • 原文来自李林峰的《多线程并发编程在 Netty 中的应用分析》 Java内存模型 工作内存和主内存 Java 内存模型规定所有的变量都存储在主内存中(JVM 内存的一部分),每 个线程有自己独立的工作内存,它保存了被该线程...
  • 在看《java并发编程实战》时的一些知识点: 静态工厂方法:以前遇到过,应该是有笔记记录的 工作单元 和 执行机制: 什么是工作单元? 什么是执行机制? 搜集的一些博客的整理: Java多线程常用面试题(含...
  • java多线程并发编程例子

    热门讨论 2012-03-08 12:11:38
    关于java.util.concurrent多线程核心包内各种线程资源的使用场景例子
  • 文章目录第一章 多线程并发编程第二节 线程安全问题1.2.1 线程安全之可见性问题 第一章 多线程并发编程 第二节 线程安全问题 1.2.1 线程安全之可见性问题 学习中。。。未完待续 ...
  • Java多线程并发编程实践总结

    千次阅读 2019-08-19 23:19:59
    学过Java并发编程的小伙伴们,在实际编程中,需要考虑以下点: 1.使用本地变量 2.使用不可变量类 3.最小化锁的作用范围 4.使用线程池,而不是直接使用new thread进行,通常创建一个线程是昂贵的。 5.宁可使用...
  • 线程和进程 进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。 程序运行时系统就会创建一个进程,...一个进程可以由很线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变...
  • 1.java并发编程(1)--线程 可见性 volatile怎么用
  • 多线程并发编程】七 线程的优先级

    千次阅读 多人点赞 2020-02-26 12:56:15
    介绍一些线程优先级的小知识
  • 十个多线程并发编程面试题(附答案)

    千次阅读 多人点赞 2021-02-06 11:29:10
    1.说说你知道的创建线程的方式 1、继承Thread类,重写run方法。2、实现Runnable接口,重写run方法。3、实现Callable接口,重写call方法。4、通过线程池创建线程。 2.说说Runnable和Callable的区别 Callable可以...
  • Java 多线程 并发编程 整理

    千次阅读 2018-04-09 11:42:20
    一、多线程 1、操作系统有两个容易混淆的概念,进程和线程。 进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据;不同进程的地址空间是互相隔离的;进程拥有各种...
  • 多线程并发编程】八 线程的礼让和守护线程

    千次阅读 多人点赞 2020-02-27 01:27:52
    线程的礼让yield和守护线程,我们日常的开发中,都不会用到,为什么还需要了解他?
  • 多线程并发编程】六 什么是线程安全?

    千次阅读 多人点赞 2020-02-24 18:57:26
    在学习多线程的道路上,我们会经常看到线程安全这类词汇,面试官也经常问,本文就来说一说什么是线程安全。
  • 在进行java 多线程并发的学习之前,有必要了解一下进程和线程的基本知识,会有助于理解后面的学习。 并行与并发 并行: 多个事件在同一时间发生 并发: 多个事件在一时间段内发生 单CPU 仅一个处理器来完成系统...
  • Java多线程并发编程,ConcurrentMap.putIfAbsent(key,value)的正确使用 bigfish 1 年前 java.util.concurrent提供并发集合(concurrent collection)。有些集合的接口通过依赖状态的修改操作(state-...
  • 文章目录第一章 多线程并发编程第一节 Java基础1.1.1 Java程序运行原理分析class文件内容JVM运行时数据区方法区堆内存虚拟机栈本地方法栈程序计数器查看class文件内容 第一章 多线程并发编程 第一节 Java基础 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 26,930
精华内容 10,772
关键字:

多线程并发编程