1、高并发线程基础,自旋锁CAS操作与volatile

看一段代码:

import java.util.concurrent.TimeUnit;

public class T01_WhatIsThread {
    private static class T1 extends Thread {
        @Override
        public void run() {
           for(int i=0; i<10; i++) {
               try {
                   TimeUnit.MICROSECONDS.sleep(1);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println("T1");
           }
        }
    }

    public static void main(String[] args) {
        new T1().run();
//        new T1().start();
        for(int i=0; i<10; i++) {
            try {
                TimeUnit.MICROSECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("main");
        }
    }
}

可以看到线程调用方法:1、run();方法,2、start();方法

这两个方法有什么区别呢?

如果调用run() 方法:可以看到T1 执行完后,才执行main线程

  

如果调用start()方法:可以看到可以交叉执行

  

线程有多个方法,如:seelp(); 休眠,  yield(); 让行  join(); 抢行

 

synchronized

锁对象

public class T {

    private int count = 10;
    private Object o = new Object();

    public void m() {
        synchronized(o) { //任何线程要执行下面的代码,必须先拿到o的锁
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
}

锁定当前对象

public class T {

    private int count = 10;

    public void m() {
        synchronized(this) { //任何线程要执行下面的代码,必须先拿到this的锁
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
}

如果锁定当前对象,其实可以直接这样写

public class T {

    private int count = 10;

    public synchronized void m() { //等同于在方法的代码执行时要synchronized(this)
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
}

锁静态的写法

public class T {

    private static int count = 10;

    public synchronized static void m() { //这里等同于synchronized(FineCoarseLock.class)
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }

    public static void mm() {
        synchronized(T.class) { //考虑一下这里写synchronized(this)是否可以?
            count --;
        }
    }
}

加锁的时候,可以不加volatile 也可以,这种的不管用volatile 或者synchronized 都可以保证有序

public class T implements Runnable {

    private /*volatile*/ int count = 100;

    @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<100; i++) {
            new Thread(t, "THREAD" + i).start();
        }
    }

}

同步和非同步方法是否可以同时调用?

public class T {

    public synchronized void m1() {
        System.out.println(Thread.currentThread().getName() + " m1 start...");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " m1 end");
    }

    public void m2() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " m2 ");
    }

    public static void main(String[] args) {
        T t = new T();
        /*new Thread(()->t.m1(), "t1").start();
        new Thread(()->t.m2(), "t2").start();*/

        new Thread(t::m1, "t1").start();
        new Thread(t::m2, "t2").start();
        /*
        //1.8之前的写法
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.m1();
            }
        });
        */
    }
}

面试题:模拟银行账户
对业务写方法加锁
对业务读方法不加锁
这样行不行?容易产生脏读问题(dirtyRead)

import java.util.concurrent.TimeUnit;

public class Account {
    String name;
    double balance;

    public synchronized void set(String name, double balance) {
        this.name = name;

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.balance = balance;
    }

    public /*synchronized*/ double getBalance(String name) {  // 这个synchronized必须加上才可以保证数据读取的正确
        return this.balance;
    }
public static void main(String[] args) { Account a = new Account(); new Thread(()->a.set("zhangsan", 100.0)).start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(a.getBalance("zhangsan")); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(a.getBalance("zhangsan")); } }

一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.也就是说synchronized获得的锁是可重入的,它必须可重入

import java.util.concurrent.TimeUnit;

public class T {
    synchronized void m1() {
        System.out.println("m1 start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        m2();      // m1方法里可以调用m2方法
        System.out.println("m1 end");
    }

    synchronized void m2() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m2");
    }

    public static void main(String[] args) {
        new T().m1();
    }
}

synchronized获得的锁是可重入的,这里是继承中有可能发生的情形,子类调用父类的同步方法

import java.util.concurrent.TimeUnit;

public class T {
    synchronized void m() {
        System.out.println("m start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m end");
    }
    public static void main(String[] args) {
        new TT().m();
    }
}

class TT extends T {
    @Override
    synchronized void m() {    // 锁的其实还是同一个对象
        System.out.println("child m start");
        super.m();
        System.out.println("child m end");
    }
}

程序在执行过程中,如果出现异常,默认情况锁会被释放
所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。
比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,
在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。
因此要非常小心的处理同步业务逻辑中的异常

import java.util.concurrent.TimeUnit;

public class T {
    int count = 0;
    synchronized void m() {
        System.out.println(Thread.currentThread().getName() + " start");
        while(true) {
            count ++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if(count == 5) {
                int i = 1/0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
                System.out.println(i);
            }
        }
    }

    public static void main(String[] args) {
        T t = new T();
        Runnable r = new Runnable() {
            @Override
            public void run() {
                t.m();
            }
        };
        new Thread(r, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(r, "t2").start();
    }
}

synchronized的底层实现
  JDK早期的: 重量级 - OS
  后来的改进:锁升级的概念:推荐看《我就是厕所所长》 (一 二)

升级步骤概念
  sync (Object)
  markword 在锁的对象头上面记录这个线程ID (偏向锁)
  如果线程争用:升级为 自旋锁
  10次以后,升级为重量级锁 - OS
  不能降级,只能升级
  执行时间短(加锁代码),线程数少,--用自旋
  执行时间长,线程数多,--用系统锁

不要锁字符常量(如:“abc”)、引用类型,如:Integer、Long

 

volatile 关键字,使一个变量在多个线程间可见
A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
使用volatile关键字,会让所有线程都会读到变量的修改值
在下面的代码中,running是存在于堆内存的t对象中
当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
使用volatile,将会强制所有线程都去堆内存中读取running的值
可以阅读这篇文章进行更深入的理解:http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized

public class T01_HelloVolatile {
    /*volatile*/ boolean running = true; //对比一下有无volatile的情况下,整个程序运行结果的区别
    void m() {
        System.out.println("m start");
        while(running) {
        }
        System.out.println("m end!");
    }

    public static void main(String[] args) {
        T01_HelloVolatile t = new T01_HelloVolatile();

        new Thread(t::m, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.running = false;
    }
}

看这段代码,虽然加了volatile,但是执行结果绝对不是10000,因为count++ 不是原子性操作

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

public class T04_VolatileNotSync {
    volatile int count = 0;
    void m() {
        for(int i=0; i<10000; i++) {
            count++;
        }
    }

    public static void main(String[] args) {
        T04_VolatileNotSync t = new T04_VolatileNotSync();

        List<Thread> threads = new ArrayList<Thread>();
        for(int i=0; i<10; i++) {
            threads.add(new Thread(t::m, "thread-"+i));
        }
        threads.forEach((o)->o.start());

        threads.forEach((o)->{
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.count);
    }
}

对比上一个程序,可以用synchronized解决,synchronized可以保证可见性和原子性,volatile只能保证可见性

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

public class T05_VolatileVsSync {
    /*volatile*/ int count = 0;

    synchronized void m() {
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }
    public static void main(String[] args) {
        T05_VolatileVsSync t = new T05_VolatileVsSync();

        List<Thread> threads = new ArrayList<Thread>();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }

        threads.forEach((o) -> o.start());

        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.count);
    }
}

synchronized优化:同步代码块中的语句越少越好,比较m1和m2

import java.util.concurrent.TimeUnit;

public class FineCoarseLock {

    int count = 0;

    synchronized void m1() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
        count ++;
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    void m2() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
        //采用细粒度的锁,可以使线程争用时间变短,从而提高效率
        synchronized(this) {
            count ++;
        }
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

锁定某对象o,如果o的属性发生改变,不影响锁的使用,但是如果o变成另外一个对象,则锁定的对象发生改变,应该避免将锁定对象的引用变成另外的对象

import java.util.concurrent.TimeUnit;

public class SyncSameObject {

    /*final*/ Object o = new Object();
    void m() {
        synchronized(o) {
            while(true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args) {
        SyncSameObject t = new SyncSameObject();
        //启动第一个线程
        new Thread(t::m, "t1").start();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //创建第二个线程
        Thread t2 = new Thread(t::m, "t2");

        t.o = new Object(); //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会
        t2.start();
    }
}

解决同样的问题的更高效的方法,使用AtomXXX类
AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的

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

public class T01_AtomicInteger {
    /*volatile*/ //int count1 = 0;
    AtomicInteger count = new AtomicInteger(0);

    /*synchronized*/ void m() {
        for (int i = 0; i < 10000; i++)
            //if count1.get() < 1000
            count.incrementAndGet(); //count1++,自增
    }

    public static void main(String[] args) {
        T01_AtomicInteger t = new T01_AtomicInteger();

        List<Thread> threads = new ArrayList<Thread>();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }
        threads.forEach((o) -> o.start());

        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.count);
    }
} 

CAS  (compare and set)--比较并且设定,无锁优化,自旋、乐观锁

cas 是cpu原语的支持,过程中不能打断

ABA问题,解决方案:每次执行时候加上版本号。可以用AtomicStampedReference解决,加上时间戳。

Unsafe类:等同于C和C++,可以直接操作java虚拟机里面的内存,它只能用反射才能使用:参考:https://blog.csdn.net/zyzzxycj/article/details/89877863

所有的AtomicXXX类,内部使用的都是compareAndSetXXX方法,这些方法都用的Unsafe类里的方法,Unsafe 类是final 的,所以要用才能反射使用。

   示例代码:AtomicInteger的使用

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

public class T01_AtomicInteger {
    /*volatile*/ //int count1 = 0;
    
    AtomicInteger count = new AtomicInteger(0); 

    /*synchronized*/ void m() { // 原来要加synchronized,现在不需要加了
        for (int i = 0; i < 10000; i++)
            //if count1.get() < 1000
            count.incrementAndGet(); //count1++
    }

    public static void main(String[] args) {
        T01_AtomicInteger t = new T01_AtomicInteger();

        List<Thread> threads = new ArrayList<Thread>();

        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }

        threads.forEach((o) -> o.start());

        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println(t.count);
    }
}

点进去看AtomicInteger的源码,里面有个compareAndSet方法

    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

compareAndSet方法里调用的是unsafe类的方法,所以最终调用的是unsafe类,版本问题,有的里面可以是compareAndSwap方法,有的可能是compareAndSet方法,

compareAndSet方法里有3个参数:要改的值、期望的值、设置的新值

如:cas方法(V, Expected, NewValue),

如果第一个参数是要改的值,比如是0,

第二个参数是期望的值,比如现在是3,也就是要这个值的之前,它是3,

第三个参数是1,也就是要把0改成1,

但是如果这个时候有其他线程已经把某个变量改成4了,但是它的期望值是3,所以就会失败,

如果期望值是3,并且中间没有其他线程改这个值,满足期望值的条件,就会修改成功。

 

如果在修改复制操作的一瞬间,有其他线程要修改这个变量的值,是否会出问题?

不会的,CAS是CPU原语的支持,在执行过程中不允许被其他线程给打断,所以不会出现其他线程的操作。

 

ABA问题:

在比对期望值的时候,虽然期望值一样的,但是这个过程中有很多线程,

比如期望值是A,其他线程已经改成了B,但是又改成了A,等对比期望值的时候,还以为一直是A,没有发生过改变。

如果是基本数据类型,无所谓,但如果是引用类型,可能指向的对象已经发生改变。

解决方案:

1. 每次执行时候加上版本号,任何线程在执行完以后,要在对应的版本号加1,每次不光比对期望值,还要比对版本号。

2. 可以用AtomicStampedReference解决,加上时间戳。

 

posted @ 2021-02-19 18:05  aBiu--  阅读(99)  评论(0编辑  收藏  举报