多线程总结(一)

一、Thread类的应用

  线程的生命周期:

            创建,就绪,运行,阻塞,死亡;

 

下面是Thread类中常用的方法:

  以下是关系到线程运行状态的几个方法:

1)start方法

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

2)run方法

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

3)sleep方法

  sleep方法有两个重载版本:

1

2

3

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

33

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方法相当于让线程进入阻塞状态。

4)yield方法

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

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

5)join方法

  join方法有三个重载版本:

1

2

3

join()

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

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

   假如在main线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间。如果调用的是无参join方法,则等待thread执行完毕,如果调用的是指定了时间参数的join方法,则等待一定的时间。

  看下面一个例子:

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

public class Test {

     

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

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

        Test test = new Test();

        MyThread thread1 = test.new MyThread();

        thread1.start();

        try {

            System.out.println("线程"+Thread.currentThread().getName()+"等待");

            thread1.join();

            System.out.println("线程"+Thread.currentThread().getName()+"继续执行");

        } catch (InterruptedException e) {

            // TODO Auto-generated catch block

            e.printStackTrace();

        }

    } 

     

    class MyThread extends Thread{

        @Override

        public void run() {

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

            try {

                Thread.currentThread().sleep(5000);

            } catch (InterruptedException e) {

                // TODO: handle exception

            }

            System.out.println("线程"+Thread.currentThread().getName()+"执行完毕");

        }

    }

}

   输出结果:

  可以看出,当调用thread1.join()方法后,main线程会进入等待,然后等待thread1执行完之后再继续执行。

实际上调用join方法是调用了Object的wait方法,这个可以通过查看源码得知:

wait方法会让线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限

  由于wait方法会让线程释放对象锁,所以join方法同样会让线程释放对一个对象持有的锁。具体的wait方法使用在后面文章中给出。

6)interrupt方法

  interrupt,顾名思义,即中断的意思。单独调用interrupt方法可以使得处于阻塞状态的线程抛出一个异常,也就说,它可以用来中断一个正处于阻塞状态的线程;另外,通过interrupt方法和isInterrupted()方法来停止正在运行的线程。

  下面看一个例子:

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

public class Test {

     

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

        Test test = new Test();

        MyThread thread = test.new MyThread();

        thread.start();

        try {

            Thread.currentThread().sleep(2000);

        } catch (InterruptedException e) {

             

        }

        thread.interrupt();

    } 

     

    class MyThread extends Thread{

        @Override

        public void run() {

            try {

                System.out.println("进入睡眠状态");

                Thread.currentThread().sleep(10000);

                System.out.println("睡眠完毕");

            } catch (InterruptedException e) {

                System.out.println("得到中断异常");

            }

            System.out.println("run方法执行完毕");

        }

    }

}

   输出结果:

  从这里可以看出,通过interrupt方法可以中断处于阻塞状态的线程。那么能不能中断处于非阻塞状态的线程呢?看下面这个例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

public class Test {

     

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

        Test test = new Test();

        MyThread thread = test.new MyThread();

        thread.start();

        try {

            Thread.currentThread().sleep(2000);

        } catch (InterruptedException e) {

             

        }

        thread.interrupt();

    } 

     

    class MyThread extends Thread{

        @Override

        public void run() {

            int i = 0;

            while(i<Integer.MAX_VALUE){

                System.out.println(i+" while循环");

                i++;

            }

        }

    }

}

   运行该程序会发现,while循环会一直运行直到变量i的值超出Integer.MAX_VALUE。所以说直接调用interrupt方法不能中断正在运行中的线程。

  但是如果配合isInterrupted()能够中断正在运行的线程,因为调用interrupt方法相当于将中断标志位置为true,那么可以通过调用isInterrupted()判断中断标志是否被置位来中断线程的执行。比如下面这段代码:

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

public class Test {

     

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

        Test test = new Test();

        MyThread thread = test.new MyThread();

        thread.start();

        try {

            Thread.currentThread().sleep(2000);

        } catch (InterruptedException e) {

             

        }

        thread.interrupt();

    } 

     

    class MyThread extends Thread{

        @Override

        public void run() {

            int i = 0;

            while(!isInterrupted() && i<Integer.MAX_VALUE){

                System.out.println(i+" while循环");

                i++;

            }

        }

    }

}

   运行会发现,打印若干个值之后,while循环就停止打印了。

  但是一般情况下不建议通过这种方式来中断线程,一般会在MyThread类中增加一个属性 isStop来标志是否结束while循环,然后再在while循环中判断isStop的值。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

class MyThread extends Thread{

        private volatile boolean isStop = false;

        @Override

        public void run() {

            int i = 0;

            while(!isStop){

                i++;

            }

        }

         

        public void setStop(boolean stop){

            this.isStop = stop;

        }

    }

   那么就可以在外面通过调用setStop方法来终止while循环。

7)stop方法

  stop方法已经是一个废弃的方法,它是一个不安全的方法。因为调用stop方法会直接终止run方法的调用,并且会抛出一个ThreadDeath错误,如果线程持有某个对象锁的话,会完全释放锁,导致对象状态不一致。所以stop方法基本是不会被用到的。

8)destroy方法

  destroy方法也是废弃的方法。基本不会被使用到。

  以下是关系到线程属性的几个方法:

  1)getId

  用来得到线程ID

  2)getName和setName

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

  3)getPriority和setPriority

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

  4)setDaemon和isDaemon

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

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

  Thread类有一个比较常用的静态方法currentThread()用来获取当前线程。

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

 

 

  在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。

 在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。

在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。

 

二、synchronized

当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

当多个线程同时访问临界(共享)资源(一个对象,对象中的属性,一个文件,一个数据库等)时,就可能会产生线程安全问题。

基本上所有的并发模式在解决线程安全问题时,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。

  通常来说,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。

在Java中,提供了两种方式来实现同步互斥访问:synchronized(隐式锁)和Lock(显示锁)。

解决多线程安全问题的3种方式:

1.  同步代码块   synchronized

2.  同步方法     synchronized

3.  同步锁       Lock

1.synchronized方法

 1)当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。这个原因很简单,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。

  2)当一个线程正在访问一个对象的synchronized方法,那么其他线程能访问该对象的非synchronized方法。这个原因很简单,访问非synchronized方法不需要获得该对象的锁,假如一个方法没用synchronized关键字修饰,说明它不会使用到临界资源,那么其他线程是可以访问这个方法的,

  3)如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。

2.synchronized代码块

    synchronized代码块类似于以下这种形式:

1

2

3

synchronized(synObject) {

         

    }

  当在某个线程中执行这段代码块,该线程会获取对象synObject的锁,从而使得其他线程无法同时访问该代码块。

  synObject可以是this,代表获取当前对象的锁,也可以是类中的一个属性,代表获取该属性的锁。

另外,每个类也会有一个锁,它可以用来控制对static数据成员的并发访问。

  并且如果一个线程执行一个对象的非static synchronized方法,另外一个线程需要执行这个对象所属类的staticsynchronized方法,此时不会发生互斥现象,因为访问static synchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁,所以不存在互斥现象。

最后,有一点要注意:对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。

 

三、lock

下面我们就来探讨一下java.util.concurrent.locks包中常用的类和接口。

1.Lock

  首先要说明的就是Lock,通过查看Lock的源码可知,Lock是一个接口:

1

2

3

4

5

6

7

8

public interface Lock {

    void lock();

    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();

    Condition newCondition();

}

   下面来逐个讲述Lock接口中每个方法的使用,lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。newCondition()这个方法暂且不在此讲述,会在后面的线程协作一文中讲述。

  在Lock中声明了四个方法来获取锁,那么这四个方法有何区别呢?

  首先lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。

  由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:

1

2

3

4

5

6

7

8

9

Lock lock = ...;

lock.lock();

try{

    //处理任务

}catch(Exception ex){

     

}finally{

    lock.unlock();   //释放锁

}

  tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

  tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

  所以,一般情况下通过tryLock来获取锁时是这样使用的:

1

2

3

4

5

6

7

8

9

10

11

12

Lock lock = ...;

if(lock.tryLock()) {

     try{

         //处理任务

     }catch(Exception ex){

         

     }finally{

         lock.unlock();   //释放锁

     } 

}else {

    //如果不能获取锁,则直接做其他事情

}

   lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

  由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。

  因此lockInterruptibly()一般的使用形式如下:

1

2

3

4

5

6

7

8

9

public void method() throws InterruptedException {

    lock.lockInterruptibly();

    try {  

     //.....

    }

    finally {

        lock.unlock();

    }  

}

  注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。

  因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。

  而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

2.ReentrantLock

https://www.2cto.com/kf/201603/493718.html   ReentrantLock常用方法说明

3.ReadWriteLock

ReadWriteLock也是一个接口,在它里面只定义了两个方法:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public interface ReadWriteLock {

    /**

     * Returns the lock used for reading.

     *

     * @return the lock used for reading.

     */

    Lock readLock();

 

    /**

     * Returns the lock used for writing.

     *

     * @return the lock used for writing.

     */

    Lock writeLock();

}

   一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。

4.ReentrantReadWriteLock

    互斥锁一次只允许一个线程访问共享数据,哪怕进行的是只读操作;读写锁允许对共享数据进行更高级别的并发访问:对于写操作,一次只有一个线程(write线程)可以修改共享数据,对于读操作,允许任意数量的线程同时进行读取。与互斥锁相比,使用读写锁能否提升性能则取决于读写操作期间读取数据相对于修改数据的频率,以及数据的争用——即在同一时间试图对该数据执行读取或写入操作的线程数。

读写锁对于读多写少的情况,相比较synchronized,效率提升很多

 ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。

 

5.Lock和synchronized的选择

  总结来说,Lock和synchronized有以下几点不同:

  1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

  2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

  3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

  4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

  5)Lock可以提高多个线程进行读操作的效率。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。三.

Si、三.锁的相关概念介绍三.锁的

四、锁的相关概念

在前面介绍了Lock的基本使用,这一节来介绍一下与锁相关的几个概念。

1.可重入锁

  如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。

  看下面这段代码就明白了:

1

2

3

4

5

6

7

8

9

class MyClass {

    public synchronized void method1() {

        method2();

    }

     

    public synchronized void method2() {

         

    }

}

   上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。

  而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。

2.可中断锁

  可中断锁:顾名思义,就是可以相应中断的锁。

  在Java中,synchronized就不是可中断锁,而Lock是可中断锁。

  如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

  在前面演示lockInterruptibly()的用法时已经体现了Lock的可中断性。

3.公平锁

  公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。

  非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。

  在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。

  而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

  看一下这2个类的源代码就清楚了:

  在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。

  我们可以在创建ReentrantLock对象时,通过以下方式来设置锁的公平性:

1

ReentrantLock lock = new ReentrantLock(true);

   如果参数为true表示为公平锁,为fasle为非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。

  另外在ReentrantLock类中定义了很多方法,比如:

  isFair()       //判断锁是否是公平锁

  isLocked()   //判断锁是否被任何线程获取了

  isHeldByCurrentThread()  //判断锁是否被当前线程获取了

  hasQueuedThreads()  //判断是否有线程在等待该锁

  在ReentrantReadWriteLock中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过要记住,ReentrantReadWriteLock并未实现Lock接口,它实现的是ReadWriteLock接口。

4.读写锁

  读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。

  正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。

  ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。

  可以通过readLock()获取读锁,通过writeLock()获取写锁。

  上面已经演示过了读写锁的使用方法,在此不再赘述。

五、volatile关键字解析

1、Java内存模型

想要理解volatile为什么能确保可见性,就要先理解Java中的内存模型是什么样的。

Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

基于此种内存模型,便产生了多线程编程中的数据“脏读”等问题。

举个简单的例子:在java中,i=10;执行下面这个语句:

i++;

执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中。

比如同时有2个线程执行这段代码,假如初始时i的值为10,那么我们希望两个线程执行完之后i的值变为12。但是事实会是这样吗?

可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的工作内存当中,然后线程1进行加1操作,然后把i的最新值11写入到内存。此时线程2的工作内存当中i的值还是10,进行加1操作之后,i的值为11,然后线程2把i的值写入内存。

最终结果i的值是11,而不是12。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

那么如何确保共享变量在多线程访问时能够正确输出结果呢?

在解决这个问题之前,我们要先了解并发编程的三大概念:原子性,有序性,可见性。

2、volatile的实现原理

1.可见性

处理器为了提高处理速度,不直接和内存进行通讯,而是将系统内存的数据独到内部缓存后再进行操作,但操作完后不知什么时候会写到内存。

如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了如果有其他线程对声明了volatile变量进行修改,则立即更新主内存中数据。

但这时候其他处理器的缓存还是旧的,所以在多处理器环境下,为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。 这一步确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的。

多先线程中为保证共享变量的一致性,(在一个线程中修改了共享变量的值,其他线程改变量的值随之更新),可用volatile来修饰此变量,保证内存可见性;volatile相较于synchronized是一种轻量级的同步实现策略;

差别:1.volatitle不具备互斥性(同一时间只允许一个线程访问,多线程排队访问);

2.volatile不能保证变量的原子性

 

2.有序性

Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

3、volatile的应用场景

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

1)对变量的写操作不依赖于当前值

2)该变量没有包含在具有其他变量的不变式中

下面列举几个Java中使用volatile的几个场景。

①.状态标记量

1

2

3

4

5

6

7

8

9

volatile boolean flag = false;

 //线程1

while(!flag){

    doSomething();

}

  //线程2

public void setFlag() {

    flag = true;

}

根据状态标记,终止线程。

②.单例模式中的double check

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

class Singleton{

    private volatile static Singleton instance = null;

    private Singleton() {

    }

    public static Singleton getInstance() {

        if(instance==null) {

            synchronized (Singleton.class) {

                if(instance==null)

                    instance = new Singleton();

            }

        }

        return instance;

    }

}

为什么要使用volatile 修饰instance?

主要在于instance= new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:

1.给 instance 分配内存

2.调用 Singleton 的构造函数来初始化成员变量

3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

volatile关键字保证了操作的可见性,但是volatile不能保证对变量的操作是原子性的。

原子变量:jdk1.5之后:Java.util.concurrent.atomic包下提供了大量常用的原子变量;

1.  volatile保证了内存的可见性

2.  CAS算法保证了数据的原子性

CAS加密算法:

Cas算法是硬件对于并发操作共享数据的支持,cas算法包括3个操作数:

内存值V     现在的内存值

预估值A     操作前读取的内存旧值

更新值B    

当且仅当V==A时V=B,否则不进行任何操作;

Compare-and-swap:比较和替换

可以保证线程间的同步效果,并且执行效率比synchronized要高;

六、深入解析ThreadLocal

ThreadLocal是如何为每个线程创建变量的副本的

  首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

  初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

 

应用场景:

 最常见的ThreadLocal使用场景为用来解决 数据库连接、Session管理等。

  如:

1

2

3

4

5

6

7

8

9

10

private static ThreadLocal<Connection> connectionHolder

= new ThreadLocal<Connection>() {

public Connection initialValue() {

    return DriverManager.getConnection(DB_URL);

}

};

public static Connection getConnection() {

return connectionHolder.get();

}

   下面这段代码摘自:

http://www.iteye.com/topic/103804

1

2

3

4

5

6

7

8

9

10

11

12

13

14

private static final ThreadLocal threadSession = new ThreadLocal();

public static Session getSession() throws InfrastructureException {

    Session s = (Session) threadSession.get();

    try {

        if (s == null) {

            s = getSessionFactory().openSession();

            threadSession.set(s);

        }

    } catch (HibernateException ex) {

        throw new InfrastructureException(ex);

    }

    return s;

}

 

七、同步容器

在Java中,同步容器主要包括2类:

  1)Vector、Stack、HashTable

  2)Collections类中提供的静态工厂方法创建的类

 Vector实现了List接口,Vector实际上就是一个数组,和ArrayList类似,但是Vector中的方法都是synchronized方法,即进行了同步措施。

  Stack也是一个同步容器,它的方法也用synchronized进行了同步,它实际上是继承于Vector类。

  HashTable实现了Map接口,它和HashMap很相似,但是HashTable进行了同步处理,而HashMap没有。

  Collections类是一个工具提供类,注意,它和Collection不同,Collection是一个顶层的接口。在Collections类中提供了大量的方法,比如对集合或者容器进行排序、查找等操作。最重要的是,在它里面提供了几个静态工厂方法来创建同步容器类,如下图所示:

从同步容器的具体实现源码可知,同步容器中的方法采用了synchronized进行了同步,那么很显然,这必然会影响到执行性能,另外,同步容器不能完全做到线程安全。

八、ConcurrentModificationException异常 并发修改异常

对Vector、ArrayList在迭代的时候如果同时对其进行修改就会抛出java.util.ConcurrentModificationException异常。

通过分析源码发现此异常的产生原因:

调用list.remove()方法导致modCount和expectedModCount的值不一致。

注意,像使用for-each进行迭代实际上也会出现这种问题。

解决办法:

单线程修改

在迭代器中如果要删除元素的话,需要调用迭代器的remove方法。

多线程修改

1)在使用iterator迭代的时候使用synchronized或者Lock进行同步;

2)使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。

九、并发容器ConcurrentHashMap

Java5.0开始针对多线程并发访问设计,提供了并发性能较好的并发容器,引入了java.util.concurrent包。与Vector和Hashtable、Collections.synchronizedXxx()同步容器等相比,util.concurrent中引入的并发容器主要解决了两个问题: 
1)根据具体场景进行设计,尽量避免synchronized,提供并发性。 
2)定义了一些并发安全的复合操作,并且保证并发环境下的迭代操作不会出错。

ConcurrentHashMap代替同步的Map(Collections.synchronized(new HashMap())),众所周知,HashMap是根据散列值分段存储的,同步Map在同步的时候锁住了所有的段,而ConcurrentHashMap加锁的时候根据散列值锁住了散列值锁对应的那段,因此提高了并发性能。ConcurrentHashMap也增加了对常用复合操作的支持,比如"若没有则添加":putIfAbsent(),替换:replace()。这2个操作都是原子操作。

(写入并复制,每次写入时都会先复制一份,再进行添加。因此添加操作多时,效率会比较低)CopyOnWriteArrayList和CopyOnWriteArraySet分别代替List和Set,主要是在遍历操作为主的情况下来代替同步的List和同步的Set,这也就是上面所述的思路:迭代过程要保证不出错,除了加锁,另外一种方法就是"克隆"容器对象。当期望的读数和遍历数远远大于更新数时,效率远远高于ArrayList;并发迭代操作多时适合选用;

  ConcurrentLinkedQuerue是一个先进先出的队列。它是非阻塞队列。

    ConcurrentSkipListMap可以在高效并发中替代SoredMap(例如用Collections.synchronzedMap包装的TreeMap)。

  ConcurrentSkipListSet可以在高效并发中替代SoredSet(例如用Collections.synchronzedSet包装的TreeMap)。

HashTalble 相对于HashMap是线程安全的,二者底层代码相同,HashTalble相对HashMap在增删改查方法上加了synchronzed方法;

不过HashTalble执行效率低下,且对复合操作容易出线程不安全问题;

 

 

例如:执行第一句时获得了锁,在执行第二句之前锁被其他线程获取到了,并且完成了添加操作。则执行第二句时就会出现问题;

 

JDK1.8之后ConcurrentHashMap底层也采用CAS来实现(1.5-17使用segment锁分段机制来实现,把Map分段加锁;);

支持多线程并发访问,

 

 

 

多线程的使用,会时内存开销较大。设计线程的上下文切换,创建销毁。。。。。。

 

数据结构:

数组与链表:

数组:存储多个同一种数据的容器。元素都有编号方便获取;

在建立数组的时候数组的长度就是确定的;数组是使用一块连续的内存空间保存数据

链表:

CAS加密算法:

十、线程池的使用

频繁的创建和销毁线程非常耗费资源,

线程池提供了一个线程队列,队列中保存着所有等待状态的线程,避免了创建于销毁的额外开销,提高了响应的速度;

线程的体系结构:

Java.Util.concurrent.Executor:负责线程的使用与调度的根接口;

         |--ExecutorService子接口

                   |--ThreadPoolExecutor线程池的实现类

                  |--ScheduledExecutorService 用于线程调度的子接口

                   |--ScheduledThreadPoolExecutor接口的实现类 继承了ThreadPoolExecutor实现了ScheduledExecutorService

         通过Executors工具类可创建四种线程池,分别为: 
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 
newScheduledThreadPool 创建一个定长线程池,支持(延迟或)定时及周期性任务执行。 
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO, 优先级)执行。

例:

//创建线程

ExecutorServiceexecutor = Executors.newCachedThreadPool();

//为线程池中的线程,分配任务

executor.submit(task);//这里task实现Callable的话,可返回一个Future的结果

//关闭线程池

executor.shutdown();//等待现有任务执行完成后,关闭线程池,对于此后传入的任务则不再执行

// executor.shutdownNow();

十一、CountDownLatch、CyclicBarrier和Semaphore

CountDownLatch  闭锁

CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。

public void await() throws InterruptedException { };   //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行

public boolean await(long timeout, TimeUnitunit) throws InterruptedException { };  //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行

public void countDown() { };  //将count值减1

ps:构造方法传入一个int值,调用countDown()方法,计数减1。调用await()方法,会使当前线程挂起,直至计数器的值为0时才会继续执行;可用于多线程中挂起某一个线程,实现其他线程执行完毕或到达某一个状态后再继续执行;

 

CyclicBarrier

字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。

然后CyclicBarrier中最重要的方法就是await方法,它有2个重载版本:

1

2

public int await() throws InterruptedException, BrokenBarrierException { };

public int await(long timeout, TimeUnit unit)throws InterruptedException,BrokenBarrierException,TimeoutException { };

   第一个版本比较常用,用来挂起当前线程,直至所有线程都到达barrier状态(被挂起)再同时执行后续任务;

Ps:构造方法传入一个int值,调用await()方法可以挂起当前线程,直到CyclicBarrier里的所有线程都被挂起后,再同时执行后续的任务;

在初次的4个线程越过barrier状态后,又可以用来进行新一轮的使用。而CountDownLatch无法进行重复使用。

第二个版本是让这些线程等待至一定的时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务。

Semaphore

Semaphore翻译成字面意思为信号量,Semaphore可以控制同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

public Semaphore(int permits){          //参数permits表示许可数目,即同时可以允许多少线程进行访问

    sync= new NonfairSync(permits);

}

public Semaphore(int permits, boolean fair) {    //这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可

    sync= (fair)? new FairSync(permits) : new NonfairSync(permits);

}

下面说一下Semaphore类中比较重要的几个方法,首先是acquire()release()方法:

 

1

2

3

4

public void acquire() throws InterruptedException {  }     //获取一个许可

public void acquire(int permits) throws InterruptedException { } //获取permits个许可

public void release() { }          //释放一个许可

public void release(int permits) { }    //释放permits个许可

acquire()用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。

release()用来释放许可。注意,在释放许可之前,必须先获获得许可。

1

2

3

4

public boolean tryAcquire() { };    //尝试获取一个许可,若获取成功,则立即返回true,

若获取失败,则立即返回false

public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { };  

//尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false

public boolean tryAcquire(int permits) { }; //尝试获取permits个许可,若获取成功,

则立即返回true,若获取失败,则立即返回false

public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws 

InterruptedException { }; 

//尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false

 

 

 这4个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:

   另外还可以通过availablePermits()方法得到可用的许可数目。Semaphore是计数信号量。Semaphore管理一系列许可证。每个acquire方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个release方法增加一个许可证,这可能会释放一个阻塞的acquire方法。然而,其实并没有实际的许可证这个对象,Semaphore只是维持了一个可获得许可证的数量。 
Semaphore经常用于限制获取某种资源的线程数量。下面举个例子,比如说操场上有5个跑道,一个跑道一次只能有一个学生在上面跑步,一旦所有跑道在使用,那么后面的学生就需要等待,直到有一个学生不跑了,

Ps:下面对上面说的三个辅助类进行一个总结:

1CountDownLatchCyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;

    而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;

    另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

2Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。

十二、Callable、Future和FutureTask

常用的创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。

  如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。

而自从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。

Future:

Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

Future类位于java.util.concurrent包下,它是一个接口:

1

2

3

4

5

6

7

8

public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)

        throws InterruptedException, ExecutionException, TimeoutException;

}

   在Future接口中声明了5个方法,下面依次解释每个方法的作用:

·        cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunningtrue还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunningtrue还是false,肯定返回true

·        isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true

·        isDone方法表示任务是否已经完成,若任务完成,则返回true

·        get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;

·        get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null

  也就是说Future提供了三种功能:

1)判断任务是否完成;

2)能够中断任务;

3)能够获取任务执行结果。

  因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask

创建执行线程的方式有四种:

1. 继承Thread

2. 实现Runnable接口

3. 实现Callable接口

Runnable内的方法为run()无返回值;

Callable<v> 内的方法为 call()有返回值v,并且可以抛出异常;

4. 创建线程池

十三、线程间协作的两种方式:wait、notify、notifyAll和Condition

 wait、notify、notifyAll

从这三个方法的文字描述可以知道以下几点信息:

1wait()notify()notifyAll()方法是本地方法,并且为final方法,无法被重写。

2)调用某个对象的wait()方法能让当前线程阻塞,并且当前线程必须拥有此对象的monitor(即锁)

3)调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程;

4)调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程;

Condition

Condition是在java 1.5中才出现的,它用来替代传统的Objectwait()notify()实现线程间的协作,相比使用Objectwait()notify(),使用Conditionawait()signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,在阻塞队列那一篇博文中就讲述到了,阻塞队列实际上是使用了Condition来模拟线程间协作。

·        Condition是个接口,基本的方法就是await()signal()方法;

·        Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() 

·         调用Conditionawait()signal()方法,都必须在lock保护之内,就是说必须在lock.lock()lock.unlock之间才可以使用

Conditon中的await()对应Objectwait()

Condition中的signal()对应Objectnotify()

Condition中的signalAll()对应ObjectnotifyAll()

生产者-消费者模型的实现

1.   使用Objectwait()notify()实现:

2.      使用Condition实现

executor.submit(futureTask);

executor.shutdown();

synchronized锁的对象(this)选择

ReadWriteLock :读写,写写互斥;读读不互斥;

猜你喜欢

转载自blog.csdn.net/java_green_hand0909/article/details/80255055
今日推荐