一、前言

多线程的知识点真多啊,整理了一下,发现还是有很多不懂的地方(脑子有点乱,乱的我都不知道都是哪里不懂了),印象最深的,CAS的ABA问题解决方案中:

这个方法:

compareAndSet(预期值,更新值,预期时间戳,更新时间戳)

是在预期值等于更新值的时候返回true吧

那,

代码示例中:

n = new AtomicStampedReference<Integer>(0,0);
Thread t1 = new Thread(){
   public void run(){
      for(int i=0; i<1000; i++){
         int stamp;
         Integer reference;
         do{
             stamp = n.getStamp();
             reference = n.getReference();
         } while(!n.compareAndSet(reference, reference+1, stamp, stamp+1));
      }
   }
};

reference和reference+1啥时候能相同啊

目前不懂,希望大佬能够不吝赐教

二、线程安全

(一)多线程特性

多线程编程要保证满足三个特性:原子性、可见性、有序性。

1)原子性

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

2)可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。显然,对于单线程来说,可见性问题是不存在的。

3)有序性

程序执行的顺序按照代码的先后顺序执行。

(二)线程安全的定义

如果有多个线程同时运行同一个实现了Runnable接口的类,程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的;反之,则是线程不安全的。

(三)线程安全问题产生的原因

  • 多个线程在操作共享数据
  • 操作共享数据的线程代码有多条
  • 多个线程对共享数据有写操作

(四)解决方法——线程同步

Java中7种线程同步机制

  1. 同步代码块(synchronized)
  2. 同步方法(synchronized)
  3. Lock类
  4. 特属域变量(volatile)
  5. 局部变量(ThreadLocal)
  6. 阻塞队列(LinkedBlockingQueue)
  7. 原子变量(Atomic)

1、同步代码块(synchronized)

synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

语法:

synchronized(同步锁){
     需要同步操作的代码
}

同步锁:对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁

  • 锁对象可以是任意类型。
  • 多个线程要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

示例代码:

package com.multithread.thread;

public class Ticket implements Runnable {
    private int ticktNum = 100;

    //定义锁对象,可以是任意类型,但一定要是对象
    Object obj = new Object();

    public void run() {
        while(true){
            synchronized (obj){//同步代码块
                if(ticktNum > 0){
                    //1.模拟出票时间
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //2.打印进程号和票号,票数减1
                    String name = Thread.currentThread().getName();
                    System.out.println("线程"+name+"售票:"+ticktNum--);
                }
            }
        }
    }
}

2、同步方法(synchronized)

使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

语法:

public synchronized void method(){
   //可能会产生线程安全问题的代码 
}

那么,这种方式没有了自定义的同步锁,锁是谁呢?

  • 对于非static方法,同步锁就是this(就是调用同步方法的类对象)
  • 对于static方法,同步锁是当前方法所在类的字节码对象(类名.class)

示例代码:

package com.multithread.thread;

public class Ticket implements Runnable {
    private int ticktNum = 100;

    //定义锁对象
  //  Object obj = new Object();这种方式不需要自定义同步锁

    public void run() {
        while(true){
            sellTicket();
        }
    }

    private synchronized void sellTicket(){//同步方法
        if(ticktNum > 0){
            //1.模拟出票时间
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //2.打印进程号和票号,票数减1
            String name = Thread.currentThread().getName();
            System.out.println("线程"+name+"售票:"+ticktNum--);
        }
    }
}

3、Lock类

java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

3.1Lock接口关系图

  • Lock和ReadWriteLock是两大锁的根接口

  • Lock 接口支持重入、公平等的锁规则:实现类 ReentrantLock、ReadLock和WriteLock。
    ReadWriteLock 接口定义读取者共享而写入者独占的锁,实现类:ReentrantReadWriteLock。

3.2Lock类同步锁方法:
public void lock() :加同步锁。
public void unlock() :释放同步锁。

示例代码:

package com.multithread.thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Ticket implements Runnable {
    private int ticktNum = 100;

    //定义锁对象:构造函数参数为线程是否公平获取锁true-公平;false-不公平,即由某个线程独占,默认是false
    Lock lock = new ReentrantLock(true);

    public void run() {
        while(true){
            lock.lock();
            try{//加锁的代码用try包起来
                //加锁
                if(ticktNum > 0){
                    //1.模拟出票时间
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //2.打印进程号和票号,票数减1
                    String name = Thread.currentThread().getName();
                    System.out.println("线程"+name+"售票:"+ticktNum--);
                }
            } finally {
                //放锁
                lock.unlock();
            }
        }
    }
}
3.3不可重入锁:

不可重入锁,即线程请求它已经拥有的锁时会阻塞。也就是不能多次加锁

3.4可重入锁

可重入锁,即线程可以进入它已经拥有的锁的同步代码块。也就是可以多次加锁

可重入锁代码示例

public class ReentrantLockTest {

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

        ReentrantLock lock = new ReentrantLock();

        for (int i = 1; i <= 3; i++) {//可以循环多次加锁
            lock.lock();
        }

        for(int i=1;i<=3;i++){
            try {

            } finally {
                lock.unlock();
            }
        }
    }
}
3.5读写锁

读写锁,即可以同时读,读的时候不能写;写操作为独占锁,不能同时写,写的时候不能读。

代码示例:

public class ReadWriteLockDemo {

    private Map<String, Object> map = new HashMap<String, Object>();
    //创建一个读写锁实例
    private ReadWriteLock rw = new ReentrantReadWriteLock();
    //创建一个读锁
    private Lock r = rw.readLock();
    //创建一个写锁
    private Lock w = rw.writeLock();

    /**
     * 读操作
     *
     * @param key
     * @return
     */
    public Object get(String key) {
        r.lock();
        System.out.println(Thread.currentThread().getName() + "读操作开始执行......");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            return map.get(key);
        } finally {
            r.unlock();
            System.out.println(Thread.currentThread().getName() + "读操作执行完成......");
        }
    }

    /**
     * 写操作
     *
     * @param key
     * @param value
     */
    public void put(String key, Object value) {
        try {
            w.lock();
            System.out.println(Thread.currentThread().getName() + "写操作开始执行......");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, value);
        } finally {
            w.unlock();
            System.out.println(Thread.currentThread().getName() + "写操作执行完成......");
        }
    }

    public static void main(String[] args) {
        final ReadWriteLockDemo d = new ReadWriteLockDemo();
        d.put("key1", "value1");
        new Thread(new Runnable() {
            public void run() {
                d.get("key1");
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                d.get("key1");
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                d.get("key1");
            }
        }).start();
    }

}

4、特属域变量(volatile)

volatile关键字

4.1作用

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

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了这个变量的值,这新值对其他线程来说是立即可见的。(注意:不保证原子性)
  • 禁止进行指令重排序。(保证变量所在行的有序性)。当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
4.2应用场景

基于volatile的作用,使用volatile必须满足以下两个条件:

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

  • 该变量没有包含在具有其他变量的不变式中,比如:

    • volatile int i=0;
      int j=0;
      i=i+j;//这就包含在了具有其他变量的不变式中,这样不行
      

常见应用场景:

1.状态量标记:

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

2.双重校验:

class Singleton{
    private volatile static Singleton instance = null;
 
    private Singleton() {
 
    }
 
    public static Singleton getInstance() {
        if(instance==null) {//两个线程可能同时拿到非空的instance
            synchronized (Singleton.class) {//线程1执行后,instance不再为空,因为instance被volatile修饰,所以线程2立马就知道,便不再满足下面的条件,无法在此创建instance对象
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

5、局部变量(ThreadLocal)

ThreadLocal

5.1作用

ThreadLocal提供线程局部变量,即为使用相同变量的每一个线程维护一个该变量的副本。

当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal,比如数据库连接Connection,每个请求处理线程都需要,但又不相互影响,就是用ThreadLocal实现。

5.2应用示例

两个线程分别转账:

package com.multithread.thread;

public class Bank {
    ThreadLocal<Integer> t = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue(){
            return 0;//设置t的初始值为0
        }
    };
    public Integer get(){
        return t.get();
    }
    public void set(){
        t.set(t.get()+10);//每次存10
    }
    
    public static void main(String[] args){
        Bank bank = new Bank();
        Transfer transfer = new Transfer(bank);
        Thread t1 = new Thread(transfer);
        Thread t2 = new Thread(transfer);
        t1.start();
        t2.start();
    }
}

class Transfer implements Runnable{
    Bank bank;
    public Transfer(Bank bank){
        this.bank = bank;
    }
    public void run() {
        for (int i=0;i<10;i++){
            bank.set();
            System.out.println(Thread.currentThread()+""+bank.get());
        }
    }
}

5.3ThreadLocal分析

  • 在ThreadLocal类中定义了一个ThreadLocalMap,每一个Thread都有一个ThreadLocalMap类型的变量threadLocals
  • threadLocals内部有一个Entry,Entry的key是ThreadLocal对象实例,value就是共享变量副本
  • ThreadLocal的get方法就是根据ThreadLocal对象实例获取共享变量副本
  • ThreadLocal的set方法就是根据ThreadLocal对象实例保存共享变量副本

6、阻塞队列(LinkedBlockingQueue)

使用阻塞队列,同步Map(LinkedBlockingQueue,ConcurrentHashMap等)

7、原子变量(Atomic)

Java的java.util.concurrent.atomic包里面提供了很多可以进行原子操作的类,分为以下四类:

  • 原子更新基本类型:AtomicInteger、AtomicBoolean、AtomicLong
  • 原子更新数组:AtomicIntegerArray、AtomicLongArray
  • 原子更新引用:AtomicReference、AtomicStampedReference等
  • 原子更新属性:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater
7.1非原子性操作问题演示

以i++为例演示非原子性操作问题:

i++并不是原子操作,而是由三个操作构成:

tp1 = i;
tp2 = tp1+1;
i = tp2;

多线程执行i++:

package com.multithread.thread;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicClass {
    static int n = 0;
    public static void main(String[] args) throws InterruptedException {
        int j = 0;
        while(j<100){
            n = 0;
            Thread t1 = new Thread(){
                public void run(){
                    for(int i=0; i<1000; i++){
                        n++;
                    }
                }
            };
            Thread t2 = new Thread(){
                public void run(){
                    for(int i=0; i<1000; i++){
                        n++;
                    }
                }
            };
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println("n的最终值是:"+n);
            j++;
        }

    }
}

执行结果:

n最终值可能不是2000

7.2原子类解决非原子性操作问题

对以上代码进行修改:

package com.multithread.thread;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicClass {
    static AtomicInteger n;
    public static void main(String[] args) throws InterruptedException {
        int j = 0;
        while(j<100){
            n = new AtomicInteger(0);
            Thread t1 = new Thread(){
                public void run(){
                    for(int i=0; i<1000; i++){
                        n.getAndIncrement();
                    }
                }
            };
            Thread t2 = new Thread(){
                public void run(){
                    for(int i=0; i<1000; i++){
                        n.getAndIncrement();
                    }
                }
            };
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println("n的最终值是:"+n);
            j++;
        }

    }
}

执行结果:

n的值一直是2000

7..3原子类CAS原理分析

7.4CAS的ABA问题及解决
7.4.1ABA问题分析

当前内存的值一开始是A,被另外一个线程先改为B然后再改为A,那么当前线程访问的时候发现是A,则认为它没有被其他线程访问过。在某些场景下这样是存在错误风险的。

7.4.2ABA问题解决

解决思路:

在比较变量值的同时,比较时间戳,其他线程修改该变量之后,该变量的时间戳也会发生改变,这样就可以知道该变量被其他线程修改过。

AtomicStampedReference解决ABA问题的方法:

  • AtomicStampedReference(初始值,时间戳):构造函数设置初始值和初始时间戳
  • getStamp:获取时间戳
  • getReference:获取预期值
  • compareAndSet(预期值,更新值,预期时间戳,更新时间戳):实现CAS时间戳和预期值的比对

代码示例:

package com.multithread.thread;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicClass {
    static AtomicStampedReference<Integer> n;
    public static void main(String[] args) throws InterruptedException {
        int j = 0;
        while(j<100){
            n = new AtomicStampedReference<Integer>(0,0);
            Thread t1 = new Thread(){
                public void run(){
                    for(int i=0; i<1000; i++){
                        int stamp;
                        Integer reference;
                        do{
                            stamp = n.getStamp();
                            reference = n.getReference();
                        } while(!n.compareAndSet(reference, reference+1, stamp, stamp+1));
                    }
                }
            };
            Thread t2 = new Thread(){
                public void run(){
                    for(int i=0; i<1000; i++){
                        int stamp;
                        Integer reference;
                        do{
                            stamp = n.getStamp();
                            reference = n.getReference();

                        } while(!n.compareAndSet(reference, reference+1, stamp, stamp+1));
                    }
                }
            };
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println("n的最终值是:"+n.getReference());
            j++;
        }

    }
}

执行结果:

n=2000

注意:采用AtomicStampedReference会降低性能,慎用。

8、总结

Synchronized和Lock区别
  • synchronized是java内置关键字,在jvm层面,Lock是个java
  • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  • synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  • 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
  • synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(公平非公平皆可)
  • Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

Q.E.D.


Read The Fucking Source Code!