Java基础、多线程(Synchronized、volatile、CAS)

多线程

  • 多线程
    • 1.1 线程的概念启动方式和常用方法
      • 1.1.1 线程的概念
      • 1.1.2 启动线程的方式
      • 1.1.3 线程的常用方法
    • 1.2 线程同步 Synchronized
      • 1.2.1 锁的是对象不是代码
      • 1.2.2 synchronized(this)
      • 1.2.3 锁定方法与非锁定方法的同步执行
      • 1.2.4 可重入锁
      • 1.2.5 异常会破坏锁
      • 1.2.6 锁升级、偏向锁、自旋锁、重量级锁
      • 1.2.7 synchronized(object)需注意的
      • 1.2.8 锁的对象用final修饰
    • 1.3 volatile
      • 1.3.1 volatile的作用
      • 1.3.2 volatile具有线程可见性不具有原子性
      • 1.3.3 细化锁
      • 1.3.4 AtomicXXX类
      • 1.3.5 CAS无锁优化、自旋锁、乐观锁
      • 1.3.6 CAS中的ABA问题
      • 1.3.7 Unsafe类
    • 1.4 单例模式的线程安全性

多线程

1.1 线程的概念启动方式和常用方法

1.1.1 线程的概念

线程:线程是cpu调度的最小单位,线程切换开销小,
进程:进程是资源分配的最小单位,进程切换开销大,一个进程包含很多线程
线程和进程的一样分为五个阶段:创建、就绪、运行、阻塞、终止。

public class T1_WhatIsThread implements Runnable{
    @Override
    public void run() {
        try {
            TimeUnit.MICROSECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("T1");
    }

    public static void main(String[] args) {
//        new T1().run();
        new Thread(new T1_WhatIsThread()).start();
        try {
            TimeUnit.MICROSECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main");
    }
}

1.1.2 启动线程的方式

  1. 继承Thread
  2. 实现Runnable
  3. 通过线程池来启动
  4. lambda表达式
public class T2_HowToCreatThread {
    //使用到了静态内部类,什么时候使用静态内部类呢?静态内部类有什么特点?
    /**
     * 当需要把一个类隐藏在另外一个类的内部并且不需要使用外部类的引用的时候就可以使用静态内部类
     * 静态内部类的特点:
     * 1.与常规内部类不同,静态内部类可以有静态域和方法
     * 2.声明在接口中的内部类默认修饰是public static
     */
    static class MyThread extends Thread{
        @Override
        public void run(){
            System.out.println("Hello myThread!");
        }
    }

    static class MyRun implements Runnable{

        @Override
        public void run() {
            System.out.println("Hello Run!");
        }
    }

    public static void main(String[] args) {
        new MyThread().start();//继承Thread类,重写其中的Run方法
        new Thread(new MyRun()).start();//实现Runnable接口实现Run方法
        new Thread(new Runnable() {//lambda表达式实现run方法,Runnable是一个函数式接口
            @Override
            public void run() {
                System.out.println("lambda表达式");
            }
        }).start();
        System.out.println("main");
    }
}

1.1.3 线程的常用方法

  1. yield 让出时间片
  2. sleep 线程休眠固定时长
  3. join 一个线程等待另外一个线程结束之后执行
  4. getState 获取线程状态,不同操作系统线程状态个数不一致
public class T3_Sleep_yield_join {
    public static void main(String[] args) {
//        testSleep();//sleep TimeWaiting状态,等待时间结束之后线程重新进入就绪状态
//        testYield();//调用Yield的线程会让出时间片,重新进入就绪状态,可能下一次调用的还是让出时间片的线程
        testJoin();//在某个线程内调用 x.join(),当前调用线程就要等待x线程执行结束之后继续执行
    }
    static void testSleep(){
        new Thread(()->{
            for (int i=0;i<100;i++){
                try {
                    Thread.sleep(500);
                    System.out.println("500ms输出一次,输出:"+i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
    static void testYield(){
        new Thread(()->{
            for (int i=0;i<10;i++){
                System.out.println("输出之后让出时间片,输出"+i);
                Thread.yield();
            }
        }).start();
        new Thread(()->{
            for (int i=0;i<10;i++){
                System.out.println("线程二输出,输出"+i);
                if (i==5){
                    Thread.yield();
                }
            }
        }).start();
    }
    static void testJoin(){
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("t2输出到5之后,要等待我执行结束,输出" + i);
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("我是t2,输出" + i);
                if (i==5){
                    try {
                        t1.join();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }
}
public class T4_ThreadState extends Thread {
    @Override
    public void run(){
        try {
            System.out.println(this.getState());
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        T4_ThreadState t4_threadState = new T4_ThreadState();
        System.out.println(t4_threadState.getState());//NEW
        t4_threadState.start();
        t4_threadState.join();
        System.out.println(t4_threadState.getState());
    }
}

1.2 线程同步 Synchronized

主要是多个线程访问同一个域的问题。

1.2.1 锁的是对象不是代码

synchronized(object){
	xxxx
	xxxx
}

锁住的不是代码而是object对象,对象头有两位专门记录锁,所以有四种形式。

如下代码对某个对象加锁:

/**
 * synchronized 关键字
 * 对某个对象加锁
 * 下面代码中synchronized锁住的对象是this,在main方法中锁住的对象就是r1
 * 对于静态方法synchronized锁住的对象是T.class
 */
public class T {
    static class R1 implements Runnable{
        @Override
        public void run() {
          methodSync();
        }
        public synchronized void methodSync(){
            for (int i=0;i<10;i++){
                try {
                    System.out.println("输出:"+i);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

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

synchronized的简单使用:

/**
 * 分析一下下面程序的输出
 * 不加synchronized的时候输出不会是 9 8 7 6 5,因为当线程会在--操作中打断,所以当某个线程--后还没将数据复制回去的时候其他线程已经开始读了
 * 加synchronized可以避免这个问题的产生,拿到锁才能执行
 */
public class T implements Runnable{
    private int COUNT=10;
    @Override
    public synchronized void run() {
        COUNT--;
        System.out.println(Thread.currentThread().getName()+" COUNT= "+COUNT);
    }

    public static void main(String[] args) {
        T t = new T();
        for (int i=0;i<5;i++){
            new Thread(t).start();
        }
    }
}

1.2.2 synchronized(this)

在实例方法中this指的是调用者,也就是某个对象。
在静态方法中this指的是 类.class,也就是说一个方法中的所有静态方法如果synchronized是方法的修饰符,那么锁住的就是这个类的class对象。

1.2.3 锁定方法与非锁定方法的同步执行

同步方法与非同步方法不是互斥的,会产生线程竞争。

/**
 * 同步方法与非同步方法能否同时调用?
 * 可以的,历程如下
 */
public class T {
    public synchronized void m()  {
        for (int i=0;i<10;i++){
            System.out.println("同步方法的:"+i);
            if (i==5){
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public void mm()  {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("这是一个非同步方法");
    }

    public static void main(String[] args) {
        T t=new T();
        new Thread(t::m,"Thread1").start();
        new Thread(t::mm,"Thread2").start();
    }
}

小面试题:模拟银行的存取款问题

/**
 * 模拟银行存款取款的问题
 * 对业务写方法加锁
 * 对业务读方法不加锁
 * 这样行不行?会产生什么问题?
 * 如果允许脏读的话这样是可以的,如果不允许脏读的话要对业务读加锁。
 */
public class Account {
    private String name;
    private double balance;

    //写方法加锁
    public synchronized void setBalance(String name,double balance){
        this.name=name;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.balance=balance;
    }
    //读方法不加锁
    public synchronized double getBalance(String name){
        return this.balance;
    }

    public static void main(String[] args) {
        Account account=new Account();
        new Thread(()->{
            account.setBalance("zjx",1000.0);
        }).start();
        new Thread(()->{
            double zjx = account.getBalance("zjx");
            System.out.println(zjx);
        }).start();
    }
}

1.2.4 可重入锁

下面程序有说明:

import java.util.concurrent.TimeUnit;

/**
 * 一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁
 * 也就是说synchronized的锁是可重入的
 * 实际上来讲,synchronized有一个计数器,多一层方法就加1
 */
public class T {
    synchronized void m1(){
        System.out.println("m1 start");
        try {
            TimeUnit.SECONDS.sleep(1);
            m2();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m1 end");
    }
    synchronized void m2(){
        System.out.println("this is m2");
        try {
            TimeUnit.SECONDS.sleep(2);
            System.out.println("m2休眠结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        T t=new T();
        new Thread(t::m1).start();
    }
}

在子类与父类的继承中也会发生锁重入:

import java.util.concurrent.TimeUnit;

/**
 *一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁
 *也就是说synchronized的锁是可重入的
 * 在子类继承父类的时候这种情形也有可能会发生
 * 在子类中调用父类的方法,父类中的this也是子类的对象,所以两个synchronized锁住的是同一个对象
 */
public class T {
    synchronized void m(){
        try {
            System.out.println("我是父类synchronized方法");
            System.out.println(this);
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class TT extends T{
    @Override
    synchronized void m(){
        System.out.println("TT start");
        try {
            TimeUnit.SECONDS.sleep(1);
            super.m();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("TT end");
    }

    public static void main(String[] args) {
        TT tt=new TT();
        new Thread(tt::m).start();
    }
}

1.2.5 异常会破坏锁

程序中出现异常,默认情况下锁会被释放,如果不想释放就加catch方法捕获,下面程序有说明:

/**
 * 程序在执行过程中,如果出现异常,默认情况下锁会被破坏
 * 所以在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况
 * 比如在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适的
 * 在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据
 * 因此要非常小心的处理同步业务逻辑中的异常
 */
public class T{
    public synchronized void m() {
        System.out.println("Thread Start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            int i=1/0;//这里不处理就会自动让出锁,抛出异常之后还会继续执行
            System.out.println(i);
        }catch (Exception e){
        }

        System.out.println("Thread End");
    }

    public static void main(String[] args) {
        T t = new T();
        for(int i=0;i<3;i++){
            new Thread(t::m).start();
        }
    }
}

1.2.6 锁升级、偏向锁、自旋锁、重量级锁

synchronized底层的实现
JDK早期的synchronized 使用的是重量级锁 OS
后来的改进:锁升级
偏向锁->自旋锁->重量级锁(找OS申请)

sync(Object)
1.markword 如果只有一个线程,记录这个线程的ID (偏向锁),此时并没有加锁
2.如果线程征用,升级为自旋锁,自旋期间占用CPU
3.如果自旋10次之后,升级为重量锁OS(进入等待队列,不占CPU)

锁只能升级不能降级

执行时间短(加锁代码),线程数比较少,用自旋锁(Lock)
执行时间长,线程数多,用系统锁(synchronized)

1.2.7 synchronized(object)需注意的

  1. object不能使用String常量
  2. 不能使用Integer、Float、Double等基本类型的包装类

1.2.8 锁的对象用final修饰

/**
 * 锁定一个对象,如果对象的属性发生改变,那锁没什么问题,不影响使用
 * 但是如果o变成另外一个对象,则锁定的对象发生改变
 * 应该避免将锁定的对象的引用变成另外一个对象
 * 所以锁的对象一般用final修饰
 */
public class T {
    final Object object=new Object();
    void m(){
        synchronized (object){
            for (int i=0;i<10;i++){
                System.out.println(i);
            }
        }
    }

    public static void main(String[] args) {
        T t=new T();
        new Thread(t::m).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
//        t.object=new Object(); //加了这个两个线程就不再具有互斥性了
        new Thread(t::m).start();
    }
}

1.3 volatile

1.3.1 volatile的作用

不加volatile的情况
副本是指操作一个例如整形变量时拷贝的值。
副本改变了flag的内容之后会马上写回共享区,但是其他线程副本什么时候检测共享区域的flag新值这个也不好控制,线程之间不可见
volatile的作用

  • 保证线程可见性
    - MESI
    - 缓存一致性协议
  • 禁止指令重排序
    - DCL单例
    - Double Check lock
/**
 * volatile  可变的,易变的
 */
public class T {
    /*volatile*/ boolean flag=true;
    void m(){
        System.out.println("m方法start");
        while (flag){
//            try {
//                Thread.sleep(500);
//                System.out.println("等待啊等待.....");
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
        }
        System.out.println("m方法end");
    }

    public static void main(String[] args) {
        T t=new T();
        new Thread(t::m).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            System.out.println("已经赋值啦");
            t.flag=false;
        }).start();
    }
}

1.3.2 volatile具有线程可见性不具有原子性

程序中有说明

import java.util.ArrayList;
import java.util.List;

/**
 * volatile并不能保证多个线程共同修改一个变量时所带来的不一致的问题,也就是说volatile不能替代synchronized
 * 下面的程序在i自加在1000以内的时候不出现问题
 * The following program does not have a problem if I is added to 1000 or less
 * But there's a multithreading problem when I gets to 10000(多线程问题)
 *
 * This program adds synchronization to avoid this problem
 */
public class T1 {
    private volatile int count;
    public void m(){
        for (int i=0;i<10000;i++){
            count++;
        }
    }

    public static void main(String[] args) {
        T1 t1=new T1();
        List<Thread> list=new ArrayList<>();
        for (int i=0;i<10;i++){
            list.add(new Thread(t1::m));
        }
        list.forEach(o->o.start());
        list.forEach(o-> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t1.count);
    }
}

1.3.3 细化锁

也就是将锁粒度变细,粒度越粗效率越低,反之。一般将业务代码放在锁之外。
如下对比:

/**
 * synchronized 优化
 * 同步代码块中的语句越少越好
 * 将锁的粒度变细
 * compare m1 and m2
 * 锁征用特别频繁的时候,细琐密度较大可以将锁粗化
 */
public class FineCoarseLock {
    int count=0;
    synchronized void m1(){
        //do sth need not sync
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //do sth need sync
        count++;
        //do sth need not sync
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    void m2(){
        //do sth need not sync
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //do sth need sync
        synchronized (this){
            count++;
        }
        //do sth need not sync
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

1.3.4 AtomicXXX类

程序中有说明:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 一些常见的运算封装在AtomicXXX类中,方便使用,保证了操作的原子性但不是使用sync实现的而是使用CAS号称无锁
 * 使用CAS保证线程安全
 * 对比之前的多线程自加程序
 */
public class T01_AtomicInteger {
    AtomicInteger integer=new AtomicInteger(0);
    void m(){
        for (int i=0;i<10000;i++){
            integer.incrementAndGet();//自增并且获取值
        }
    }

    public static void main(String[] args) {
        T01_AtomicInteger atomicInteger=new T01_AtomicInteger();
        List<Thread> list=new ArrayList<>();
        for (int i=0;i<10;i++){
            list.add(new Thread(atomicInteger::m));
        }
        list.forEach(o->o.start());
        list.forEach(o-> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(atomicInteger.integer);
    }
}

1.3.5 CAS无锁优化、自旋锁、乐观锁

AtomicXXX类中保持方法的原子性并不是使用的synchronized,而是CAS(Compare And Set)。

伪代码如下:

cas(V,Expected,newVlaue){
	if(V==Expected) 
		V=newValue;
	else
		try again or fail
}

V指的是原值,Expected是期望值,newValue是要set的新值。

1.3.6 CAS中的ABA问题

  • 如果要修改的是基本类型那么ABA问题没关系(可以通过记录版本号来解决)。
  • 但是如果是引用的话,有一个较好的比喻:你和你的前女友复合了,但是她已经不是当初的她了(也许经历了其他人)。

1.3.7 Unsafe类

Java基础、多线程(Synchronized、volatile、CAS)_第1张图片

1.4 单例模式的线程安全性

说明存在于代码之中:

/**
 * 单例模式 饿汉式
 * 类加载到内存之后就实例化一个单例,JVM保证线程安全
 * 简单使用,推荐使用!
 * 唯一缺点:不管用到与否。类装载时都要完成实例化
 */
public class Singleton {
    private static final Singleton SINGLETON=new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){return SINGLETON;}

}

/**
 * 单例模式 懒汉式
 * 按需初始化,但是线程不安全
 * 多线程调用getInstance的时候会返回不同的对象 所以要加锁synchronized
 * 加了synchronized之后,问题在于锁粒度太粗,初始化变量之前可能还有其他的业务代码,所以要降低锁粒度
 */
class Singleton1{
    private static Singleton1 SINGLETON;
    private Singleton1(){
    }
    public synchronized static Singleton1 getInstance(){
        if (SINGLETON==null){
            SINGLETON=new Singleton1();
        }
        return SINGLETON;
    }
}

/**
 * 饿汉式,降低锁粒度
 */
class Singleton2{
    private static Singleton2 SINGLE;
    private Singleton2(){}
    public static Singleton2 getInstance(){
        if (SINGLE==null){ //这一步判断会提高效率,因为大部分情况下SINGLE是不为空的
            synchronized (Singleton2.class){
                if (SINGLE==null){ //synchronized体里面必须再判断一次,只有未锁之前的一次判断还是会出问题
                    SINGLE=new Singleton2();
                }
            }
        }
        return SINGLE;
    }
}

/**
 * 上面的饿汉式单例模式已经很完美,但是在超高并发的情况下依然会出现问题
 * 问题出现在指令重排序上
 * new一个对象的历程:1.开辟空间,类内变量赋默认值 2.类内变量赋为自己的值 3.将对象地址赋值给变量
 * 当两个线程同时调用getInstance的时候,第一个线程拿到锁进行创建对象
 * 指令重排序可能将创建对象的历程打乱,在变量还未赋值的时候就将对象地址
 * 赋给了变量,这个时候第二线程过来检查到SINGLE不为空了,而第一个线程
 * 还没有将SINGLE中的变量赋为真值,第二个线程可能已经直接开始使用了,
 * 此时第二线程得到的SINGLE中的变量值可能就是系统的默认值。此时就会出
 * 现问题。
 * 解决办法:加volatile 不允许指令重排序了
 *
 * 饿汉式单例模式,双重检查,需要加volatile
 */
class Singleton3{
    private static volatile Singleton3 SINGLE;
    private Singleton3(){}
    public static Singleton3 getInstance(){
        //业务逻辑代码
        if (SINGLE==null){
            synchronized (Singleton3.class){
                if (SINGLE==null){ //双重检查
                    SINGLE=new Singleton3();
                }
            }
        }
        return SINGLE;
    }
}

会持续更新…

你可能感兴趣的:(java基础)