java 线程理解

怎么理解线程和进程

      单进程单线程:一个人在一个桌上吃菜

      单进程多线程:多个人分享同一个桌子上的菜

      多进程单线程:多个人在自己的桌上吃菜

为什么需要用到多线程

      从上面可以看出来,多线程实际上是对共享业务瓜分处理的一种机制,这样可以充分利用了硬件资源,处理器资源利用,缩短业务处理时间。 

适合场景

     主要用于计算机同时执行多个任务,而多个任务之间的执行是不互相影响,大文件的读或写处理 ,大量数据迁移,大量工单处理等 。 

使用多线程注意

但是,由于多个人共同分享一桌菜,难免会发生多个人同时吃一道菜的争抢情况,所以提前分配到各自的盘子里边,互不争抢是多线程操作重要的部分。如果控制不当,会造成数据错误、死锁等现象。

几个重要概念 

     同步和异步

          其实在上一章 BIO、NIO、AIO 区别和应用场景 已经提到过同步和异步的概念,这里在简朴的讲讲。

          同步异步的区别在于消息获得的机制,同步强调的是等待获取消息的机制,异步强调的是消息通知机制。

          举个例子说明一下

               同步:你生活费没了,需要找家里人要,你打了电话过去,发现家里没接,这个时候你不断的等待,或者中间有别的事你去办暂时搁置一下要生活费的事情,但是隔几分钟你还是主动再打到家里,直到联系上家里人解决生活费的问题,这时候你才能放下心事去干别的事情。

              异步:你生活费没了,上午发出短信告知家里人生活费没有的情况了,然后你就不关心了,该干嘛干嘛,可能过完丰富多彩的一天,到晚上,家里人发短信或者打电话通知你,钱已经打进你的账户了,至此生活费的问题解决了。

      并发和并行     

       并行是指两个或多个事件在同一时刻发生,并发是指两个或多个事件在同一个时间间隔发生。似乎不太好理解,那这么理解呢?并行在多台设备上同时处理多个任务,并发是在同一个设备上同时进行多个任务。 并发是一次处理很多事情,并行是同时做很多事情。

     临界区

      用来表示一种公共资源或者共享数据,可以被多个线程使用。但每一次只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。可以理解为临界区就是在同一时刻只能有一个任务访问的代码区。

 

线程的四种状态

线程四种状态:运行、就绪、挂起、结束

阻塞和非阻塞

    通常用来形容线程之间的相互影响,如果一个线程占用了这个共享的资源,其它需要用到这块资源的线程必须在临界区外进行等待,等待会导致阻塞。如果占用资源线程一直不释放资源,其他线程都不能工作。如下图可见,线程运行状态,当即将进入临界区发现资源被占用,会进入阻塞状态(参考上图 线程4中状态图),线程挂起,排队等待资源释放。

 死锁

   比较常见的例子: 5个人,5根筷子。每个人左右手各抓一根(筷子是成双使用),但是所抓住的筷子其实另一端也被其他人抓着,5个人都不肯放弃手中的筷子,造成了有饭不能吃的尴尬局面。

死锁造成的原因:A线程持有锁L1申请持有锁L2,B线程持有锁L2申请持有锁L1,此时A线程等待L2被释放,B线程等待L1被释放,但是L2需要B线程处理完业务才能释放资源,L1需要A线程处理完业务才能释放资源,所以A和B同时等待,并且一直等待下去。

public class KuaiZi {
    String name;
    public KuaiZi(String name) {
        this.name = name;
    }

    public synchronized void eat(){
        System.out.println("use kuaizi to eat !");
    }
    public synchronized void eatWithAnotherKuaizi(KuaiZi kuaiZi){
        try {
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("我"+this.name+"要"+kuaiZi.name+"这只筷子才能吃东西");
        kuaiZi.eat();
    }
}



public class Test {

    public static void main(String[] args) {
        final KuaiZi kuaiZi1 = new KuaiZi("kuaiZi1");
        final KuaiZi kuaiZi2 = new KuaiZi("kuaiZi2");
        Thread th1 = new Thread(new Runnable() {
            @Override
            public void run() {
                kuaiZi1.eatWithAnotherKuaizi(kuaiZi2);
            }
        });
        Thread th2 = new Thread(new Runnable() {
            @Override
            public void run() {
                kuaiZi2.eatWithAnotherKuaizi(kuaiZi1);
            }
        });
        th1.start();
        th2.start();
    }
}

输出结果:

      我kuaiZi1要kuaiZi2这只筷子才能吃东西
      我kuaiZi2要kuaiZi1这只筷子才能吃东西

几个常用关键字、方法、类

    synchronized

    用来实现线程同步的关键字。

synchronized(obj){
      doSomething()
     }

     同步代码块,其他线程必须获得对象obj的锁,才能进入到代码块中doSomething。可以针对任意代码块,且可以对任意对象上锁,灵活性较高。

     

     public synchronized void withdraw(double money)

 同步方法,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程会造成阻塞。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态,有效避免了类成员变量的访问冲突,但一旦方法体过大且耗时过长,会大大影响效率。

synchronized(TestSynchronized.class) 


public static synchronized void test2() 

同步类,这样可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成员变量的访问。

synchronized(this)

    当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。这个没什么异议。

   但是,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。这个地方需要注意了。

Lock

 同步锁,为什么有了synchronized还需要Lock?

 从上边的描述可以直到,被synchronized修饰的代码块,只能持有对象的线程能运行,其他线程需要等待持有对象线程运行完并释放了对象锁才能竞争锁。

 释放的条件:1、运行完代码块,自动释放  2、遇到异常,等待jvm释放

问题来了,如果遇到等待IO或者其它比如sleep情况而被阻塞,但是又不会释放资源锁,其他线程只能眼巴巴的等待下去。此时需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。

试想这种场景:多线程进行文件的读和写操作,写和写操作也会发生冲突,但是读和读操作就不会。

如果多个线程都只是进行读操作,但是采用synchronized关键字来实现同步的话,就会造成当一个线程在进行读操作时,其他线程只能等待无法进行读操作。因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。

通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

  注意点

Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问。

Lock和synchronized有一点非常大的不同,synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

几个常用方法

ReentrantLock,意思是“可重入锁”,ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。

lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。

unLock()方法是用来释放锁的。

lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}

tryLock()方法是有返回值的,获取锁成功,则返回true,获取失败,返回false,在拿不到锁时不会一直在那等待。游刃有余。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果直接拿到,立即返回true。

lockInterruptibly()当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程

通过lockInterruptibly()方法获取某个锁时,如果不能获取到进行等待的情况下,是可以响应中断的。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

public class Test {
    private Lock lock = new ReentrantLock();   
    public static void main(String[] args)  {
        Test test = new Test();
        MyThread thread1 = new MyThread(test);
        MyThread thread2 = new MyThread(test);
        thread1.start();
        thread2.start();
         
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.interrupt();
    }  
     
    public void insert(Thread thread) throws InterruptedException{
        lock.lockInterruptibly();   //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
        try {  
            System.out.println(thread.getName()+"得到了锁");
            long startTime = System.currentTimeMillis();
            for(    ;     ;) {
                if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
                    break;
                //插入数据
            }
        }
        finally {
            System.out.println(Thread.currentThread().getName()+"执行finally");
            lock.unlock();
            System.out.println(thread.getName()+"释放了锁");
        }  
    }
}
 
class MyThread extends Thread {
    private Test test = null;
    public MyThread(Test test) {
        this.test = test;
    }
    @Override
    public void run() {
         
        try {
            test.insert(Thread.currentThread());
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"被中断");
        }
    }
}

结果:

Thread-0得到了锁
Thread-1被中断


ReadWriteLock

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接口。

ReentrantReadWriteLock

public class Test {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
     
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
    }  
     
    public void get(Thread thread) {
        rwl.readLock().lock();
        try {
            long start = System.currentTimeMillis();
             
            while(System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName()+"进行读操作");
            }
            System.out.println(thread.getName()+"读操作完毕");
        } finally {
            rwl.readLock().unlock();
        }
    }
}

输出结构:

Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-0读操作完毕
Thread-1读操作完毕

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。

volatile关键字

A、B线程都有一个自己的本地内存空间--线程栈空间,线程执行时,先把变量从主内存读取到线程自己的本地内存空间,然后再对该变量进行操作,对该变量操作完后,在某个时间再把变量刷新回主内存。

i = i + 1;

     先从主内存中获取i的值,复制一份到本地内存,然后cpu执行指令对i加1,将数据写入到高速缓冲,最好将i最终的值从高速缓冲中复制到主内存中。

  这个代码在单线程中运行是没有任何问题的,但在上图多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此A、B线程运行时有自己的高速缓存,初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程A进行加1操作,然后把i的最新值1写入到内存。此时线程B的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。最终结果i的值是1,而不是2。

扩展

为了解决缓存不一致性问题,通常来说有以下2种解决方法:

  1)通过在总线加LOCK锁的方式

  2)通过缓存一致性协议

这2种方式都是硬件层面上提供的方式。

  在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

  但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。

  所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

并发编程中的三个概念

  1、原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

  2、可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

//线程1执行的代码
int i = 0;
i = 10;
 
//线程2执行的代码
j = i;

由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.

  3、有序性:即程序执行的顺序按照代码的先后顺序执行。

int i = 0;              
boolean flag = false;
i = 1;                //语句1  
flag = true;          //语句2

        语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。

  指令重排序:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

int a = 10;    //语句1
int r = 2;    //语句2
a = a + 3;    //语句3
r = a*a;     //语句4

可能的执行顺序:   语句 2 - 语句1   - 语句3 - 语句4

但是不可能是:语句2-语句1-语句4-语句3 , 因为语句4需要依赖语句3

但是这种策略对都线程,就有可能产生影响了。

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

        语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

Java内存模型

  那么Java语言 本身对 原子性、可见性以及有序性提供了哪些保证呢?

      Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。

  Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

      1、原子性:

 

x = 10;         //语句1
y = x;         //语句2
x++;           //语句3
x = x + 1;     //语句4

       语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。

  语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。

  同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。

   所以上面4个语句只有语句1的操作具备原子性。

  也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

  从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

     2、可见性

       对于可见性,Java提供了volatile关键字来保证可见性。

  当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

  而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

  另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

  3、有序性

  在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

          在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

      另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

下面就来具体介绍下happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

  这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。

     第一个规则 对于程序次序规则来说,我的理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。

  第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个对象如果被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。

  第三条规则是一条比较重要的规则,也是后文将要重点讲述的内容。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

  第四条规则实际上就是体现happens-before原则具备传递性

 现在回头看volatile

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2)禁止进行指令重排序。

//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
 
//线程2
stop = true;

       在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

  那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

  但是用volatile修饰之后就变得不一样了:

  第一:使用volatile关键字会强制将修改的值立即写入主存;

  第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

  第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

  那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。那么线程1读取到的就是最新的正确的值。

  从上面知道volatile关键字保证了操作的可见性,但是volatile能保证对变量的操作是原子性吗?

public class Test {
    public volatile int inc = 0;
     
    public void increase() {
        inc++;
    }
     
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
         
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

   每次运行结果都不一致,都是一个小于10000的数字。而不是预期的10000

 volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。

  在前面已经提到过,自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:

  假如某个时刻变量inc的值为10,

  线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;

  然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。

  然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。

  那么两个线程分别进行了一次自增操作后,inc只增加了1。

  解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。这个就是上面的happens-before规则中的volatile变量规则,但是要注意,线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。

  根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的

  把上面的代码改成以下任何一种都可以达到效果:

public class Test {
    public  int inc = 0;
    
    public synchronized void increase() {
        inc++;
    }
    
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}
public class Test {
    public  int inc = 0;
    Lock lock = new ReentrantLock();
    
    public  void increase() {
        lock.lock();
        try {
            inc++;
        } finally{
            lock.unlock();
        }
    }
    
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}
public class Test {
    public  AtomicInteger inc = new AtomicInteger();
     
    public  void increase() {
        inc.getAndIncrement();
    }
    
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

       java.util.concurrent.atomic包下提供了一些原子操作类,对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。

  volatile能保证有序性吗?

       在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

          1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

  2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

  可能上面说的比较绕,举个简单的例子:

//x、y为非volatile变量
//flag为volatile变量
 
x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5

      由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

  并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

回到前面举的一个例子

//线程1:
context = loadContext();   //语句1
volatile boolean inited = true;             //语句2
 
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

  这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。

    volatile关键字的场景 

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

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

      上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

      用例:

//状态标记量

volatile boolean flag = false;
 
while(!flag){
    doSomething();
}
 
public void setFlag() {
    flag = true;
}

      

//有序性
volatile boolean inited = false;
//线程1:
context = loadContext();  
inited = true;            
 
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
    //double check    
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;
    }
}

join()、yield()

     join的用法, 在某些情况下,如果子线程里要进行大量的耗时的运算,主线程可能会在子线程执行完之前结束,但是如果主线程又需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()。

class Thread1 extends Thread
{
	public Thread1()
	{
		super("[Thread1] Thread");
	}
	public void run()
	{
		String threadName = Thread.currentThread().getName();
		System.out.println(threadName + " start.");
		try 
		{
			for(int i = 0; i < 5; i++)
			{
				System.out.println(threadName + " loop at " + i);
				Thread.sleep(1000);
			}
			System.out.println(threadName + " end.");
		} 
		catch (Exception e) 
		{
			System.out.println("exception from " + threadName + ".run");
		}
		
		
	}
}
 
class Thread2 extends Thread
{
	Thread1 t1;
	public Thread2(Thread1 thread1)
	{
		super("[Thread2] Thread");
		this.t1 = thread1;
	}
	public void run()
	{
		String threadName = Thread.currentThread().getName();
		System.out.println(threadName + " start.");
		try 
		{
			t1.join();
			System.out.println(threadName + " end.");
		} 
		catch (Exception e) 
		{
			System.out.println("exception from" + threadName + ".run");
		}
		
		
	}
}
 
 
public class JoinTest 
{
 
	/**
	 * @param args
	 */
	public static void main(String[] args) 
	{
 
		String threadName = Thread.currentThread().getName();
		System.out.println(threadName + " start.");
		Thread1 t1 = new Thread1();
		Thread2 t2 = new Thread2(t1);
		try 
		{
			t1.start();
			Thread.sleep(2000);
			t2.start();
			t2.join();
		} catch (Exception e) 
		{
			System.out.println("exception from main");
		}
		
		System.out.println(threadName + " end.");
	
	}
 
}
main start.    //主线程起动 
[Thread1] Thread start.  
[Thread1] Thread loop at 0  
[Thread1] Thread loop at 1  
[Thread2] Thread start.     
[Thread1] Thread loop at 2  
[Thread1] Thread loop at 3  
[Thread1] Thread loop at 4  
[Thread1] Thread end.  
[Thread2] Thread end.      
main end!      

t2一定在t1之后才结束,main线程则一定在t2结束之后结束,因为各自调用的join方法。

yield() 关于线程优先级

    一个线程调用yield意味着它要告诉虚拟机自己乐意让其它线程占用自己的位置。这只是一个暗示,并不保证会产生效果(使当前线程转到可运行状态,runnable)。

class Producer extends Thread
{
	public void run()
	{
		for(int i = 0; i < 5; i++)
		{
			System.out.println("i am producer: producerd item " + i);
			Thread.yield();
		}
	}
}
 
class Consumer extends Thread
{
	public void run()
	{
		for(int i = 0; i < 5; i++)
		{
			System.out.println("i am Consumer: Consumed item " + i);
			Thread.yield();
		}
	}
}
public class YieldTestDemo 
{
 
	/**
	 * @param args
	 */
	public static void main(String[] args) 
	{
		
		Thread producer = new Producer();
		Thread consumer = new Consumer();
		
		producer.setPriority(Thread.MIN_PRIORITY);
		consumer.setPriority(Thread.MAX_PRIORITY);
		
		producer.start();
		consumer.start();
	}
 
}
没有调用yeild
 I am Consumer : Consumed Item 0
 I am Consumer : Consumed Item 1
 I am Consumer : Consumed Item 2
 I am Consumer : Consumed Item 3
 I am Consumer : Consumed Item 4
 I am Producer : Produced Item 0
 I am Producer : Produced Item 1
 I am Producer : Produced Item 2
 I am Producer : Produced Item 3
 I am Producer : Produced Item 4
调用了yield
 I am Producer : Produced Item 0
 I am Consumer : Consumed Item 0
 I am Producer : Produced Item 1
 I am Consumer : Consumed Item 1
 I am Producer : Produced Item 2
 I am Consumer : Consumed Item 2
 I am Producer : Produced Item 3
 I am Consumer : Consumed Item 3
 I am Producer : Produced Item 4
 I am Consumer : Consumed Item 4

yield的作用一目了然,设置成不同的优先级,也证明了yield不会受优先级大小的影响。

sleep() 、wait()、notify()

      Thread类中的sleep静态本地方法

public static native void sleep(long millis) throws InterruptedException;  

sleep方法让当前正在运行的线程进入睡眠状态,暂时停止运行指定的单位时间。注意该线程在睡眠期间不会释放对象锁

Object类中的wait静态本地方法

public final native void wait(long timeout) throws InterruptedException;  

wait方法可以让当前线程(调用object.wait方法的那个线程)进入等待唤醒状态,该线程会处于等待唤醒状态直到另一个线程调用了object对象的notify方法或者notifyAll方法。线程进入等待唤醒状态时会释放对象锁。在被唤醒后,该线程会一直处于等待获取锁的状态直到它重新获取到锁,然后才可以重新恢复运行状态。特别注意:该方法只能在获取到对象锁之后才能调用,否则会抛出异常IllegalMonitorStateException。

Object类中的notify静态本地方法 和 wait是搭配使用的

public final native void notify();  

如在线程A中调用共享对象obj的wait方法后,在线程Q中可以调用obj的notify将线程A唤醒。如果A、B都调用了obj.wait,Q中调用obj.notify,此时会随机唤醒A、B中的一个线程。 如果要同时唤醒A和B,可以使用obj.notifyAll。

      从上边可以看到其实 sleep和wait都可以暂停线程,但是sleep来自Thread,wait来自Object,sleep只是让当前线程暂停指定的时间,和对象锁无关,而wait的作用是线程之间的信息交互,和对象锁有关。不管是哪种方式暂停的线程,都表示它暂时不需要CPU的时间片。操作系统会将时间片配给其它线程。wait需要别的线程执行notify/notifyAll才能够重新获得CPU时间片,但是sleep不用。

 

 

可重入锁

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

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

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

 可中断锁

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

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

公平锁

     公平锁即尽量以请求锁的顺序来获取锁。比如同时有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得,这种就是公平锁。非公平锁无法保证锁的获取是按照请求锁的顺序进行的,这样就可能导致某个或者一些线程永远获取不到锁。在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁

如公平锁设置

ReentrantLock lock = new ReentrantLock(true);

ReentrantLock类中还定义了很多方法:

  isFair()        //是否是公平锁

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

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

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

关于CAS和ABA

       CAS全程是compareAndSwap,是乐观锁的一种实现方式,涉及有三个操作数,内存位置(V)、预期值(A)和新值(B),如语句CAS(V,A,B),当*V等于A时,则将值替换为新值B。虽然从语言描述上来说是分为多个操作的,但实际上CAS操作是一个原子操作,是基于CPU提供的原子操作指令实现的。如下CAS实现更新的伪代码:

do {
        A=current();//*V赋予A,A为就是预期值

        B=update();//获取B作为新值

    } while(!compareAndSwap(V, A, B));

上面伪代码将*V赋予A,然后获取新值赋予B,最后进行交换判断直到成功,例如:

-123456
线程1A=5B=6   CAS:false
线程2  A=5B=7*V=7 

     两个线程同时进入循环进行进行更新操作,第一轮循环中只有线程2更新成功,线程1因为*V的值被线程2改变导致和预期值不一致从而失败,只能重新进入下一轮循环直到成功。

CAS解决了什么问题

     它解决了悲观锁使用了独占锁,一次只能有一个线程进入临界区的问题,在竞争状态比较低的情况下提高了并发性能。为何说是竞争低的情况下,如果上面有很多个线程同时进入循环,那么每个线程都在占用资源执行,但每次只有一个线程能更新成功。

ABA问题

如果还有一个线程3在修改,并将线程2修改后的值又变成了5,那么线程1此时是察觉不到的,它还能进行成功的执行!例如:

-123456789
线程1A=5B=6      *A=6
线程2  A=5B=7*V=7    
线程3     A=7B=5*A=5 

ABA问题解决方式:引入一个版本号,在每次更新后都加1,在上面的例子中,即时线程3将*V修改回来原值,因为版本号不一致也会导致失败重试。

CAS在java.util.concurrent有着广泛的使用,如AtomicInteger。且对于ABA问题,Java也提供了AtomicStampedReference来处理。

private static AtomicStampedReference<Integer> atomicStampedRef =
        new AtomicStampedReference<>(1, 0);
public static void main(String[] args){
    Thread main = new Thread(() -> {
        System.out.println("操作线程" + Thread.currentThread() +",初始值 a = " + atomicStampedRef.getReference());
        int stamp = atomicStampedRef.getStamp(); //获取当前标识别
        try {
            Thread.sleep(1000); //等待1秒 ,以便让干扰线程执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        boolean isCASSuccess = atomicStampedRef.compareAndSet(1,2,stamp,stamp +1);  //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
        System.out.println("操作线程" + Thread.currentThread() +",CAS操作结果: " + isCASSuccess);
    },"主操作线程");
 
    Thread other = new Thread(() -> {
        Thread.yield();
        atomicStampedRef.compareAndSet(1,2,atomicStampedRef.getStamp(),atomicStampedRef.getStamp() +1);
        System.out.println("操作线程" + Thread.currentThread() +",【increment】 ,值 = "+ atomicStampedRef.getReference());
        atomicStampedRef.compareAndSet(2,1,atomicStampedRef.getStamp(),atomicStampedRef.getStamp() +1);
        System.out.println("操作线程" + Thread.currentThread() +",【decrement】 ,值 = "+ atomicStampedRef.getReference());
    },"干扰线程");
 
    main.start();
    other.start();
}



// 输出
> 操作线程Thread[主操作线程,5,main],初始值 a = 2
> 操作线程Thread[干扰线程,5,main],【increment】 ,值 = 2
> 操作线程Thread[干扰线程,5,main],【decrement】 ,值 = 1
> 操作线程Thread[主操作线程,5,main],CAS操作结果: false

 

 

参考如:

https://blog.csdn.net/eejron/article/details/53189090

https://blog.csdn.net/sc772739805/article/details/80095666

https://blog.csdn.net/pony_maggie/article/details/43897971

https://www.cnblogs.com/dolphin0520/p/3920373.html

https://www.cnblogs.com/dolphin0520/p/3923167.html

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值