多线程笔记

1. volatile

1.1 volatile介绍

volatile保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。它在某些情况下比synchronized的开销更小。

举个例子我们来分析下面的代码:

public class Main {
    public static void main(String[] args) {
        VolatileTest volatileTest = new VolatileTest();
        new Thread(volatileTest).start();

        while (true) {
            if (volatileTest.isFlag()){
                System.out.println("over");
                break;
            }
        }
    }
}

class VolatileTest implements Runnable {
    private boolean flag;

    @Override
    public void run() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag=" + flag);
    }

    public boolean isFlag() {
        return flag;
    }
}

上面的代码最后输出结果是:

flag=true

这个结果是令人诧异的,程序会一直执行while循环不结束,flag已经为true了,为什么while循环还是不结束呢?说明这里的flag同时有了两个值

  • 在主线程中:flag=false
  • 在副线程中:flag=true

变量实际是一段内存空间,并不存在同时有两种信息的状态。其实线程在操作主存中的变量数据时,首先会将数据复制到线程私有内存中,当操作完成后才会将数据写回主存,当多个线程操作一个共享变量时,由于线程的修改,导致数据不一致性。就发生了上述结果。

为了解决共享变量的不一致性,使得多线程对共享变量的修改的可见。上述结果我们可以使用synchronized关键字来解决。如下:

 while (true) {
            synchronized (volatileTest){
                if (volatileTest.isFlag()){
                    System.out.println("over");
                    break;
                }
            }
        }

当是这样解决又有一个很大的问题,synchronized是悲观锁,使得多线程堵塞等待,极大的降低多线程的效率。那有没有一个更好的解决办法呢?这里就可以用到volatile关键字了,修改如下:

    private volatile boolean flag;

只需要将变量flag声明时,用volatile修饰就可以保证共享变量flag的可见性。再次运行就不会发生堵塞数据不一致的问题了。

注意 : 如果将代码改成下面的,运行结果也是没有问题的,导致上面的结果还有一个重要的原因,while循环中执行的太快,导致主线程来不及去主存中刷新数据。

        while (true) {
            // 只要是需要消耗一定的时间,让主线程能从主存读取数据即可
            System.out.println("no over"); 
            if (volatileTest.isFlag()) {
                System.out.println("over");
                break;
            }
        }

1.2 volatile的三大特性:

  1. 可见性
  2. 不保证原子性
  3. 禁止指令重排

具体是如何做到的可以参考以下博客
死磕Java——volatile的理解

2. Atomic

jdk1.5java.util.concurrent.atomic包下提供了常用的原子操作类,什么是原子操作呢?顾名思义,就是不可分割的操作。

  1. i++的原子性问题:i++的操作实际上分为三个步骤"读-改-写"
  int i=10;
  i=i++; //10
// 上面的代码等同于下面的
  int i = 10;
  int temp=i;
  i=i+1;
  i=temp;
// 所以最后i的值为10
  1. 原子变量:jdk1.5后java.util.concurrent.atomic包下提供了常用的原子变量:


    java.util.concurrent.atomic包
  • volatile 保证内存可见性
  • CAS(Compare-And-Swap)算法保证数据的原子性CAS算法是硬件对于并发操作共享数据的支持
    CAS包含了三个操作数:
    • 内存值V
    • 预估值A
    • 更新值B
      当且仅当V==A时,V = B,否则将不做任何操作

可参考博客:
Java中atomic包中的原子操作类总结

CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用处理器提供的CMPXCHG指令实现。

3. ConcurrentHashMap

3.1 ConcurrentHashMap 采用"锁分段"机制

Java5.0java.util.concurrent包中提供了多种并发容器类来改进同步容器的性能。ConcurrentHashMap同步容器类是Java5增加的一个线程安全的哈希表。对与多线程的操作,介于HashMapHashtable之间。内部采用“锁分段”机制替代Hashtable的独占锁。进而提高性能。此包还提供了设计用于多线程上下文中的Collection实现:
ConcurrentHashMapConcurrentSkipListMapConcurrentSkipListSetCopyOnWriteArrayListCopyOnWriteArrayset。当期望许多线程访问一个给定collection时,ConcurrentHashMap通常优于同步的HashMapConcurrentSkipListMap通常优于同步的TreeMap。当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList优于同步的ArrayList

4. CountDownLatch

4.1 CountDownLatch闭锁

CountDownLatch一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
闭锁可以延迟线程的进度直到其到达终止状态,闭锁可以用来确保某些活动直到其他活动都完成才继续执行:

  • 确保某个计算在其需要的所有资源都被初始化之后才继续执行;
  • 确保某个服务在其依赖的所有其他服务都已经启动之后才启动;
  • 等待直到某个操作所有参与者都准备就绪再继续执行。

CountDownLatch使用实例代码:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(10);
        LatchDemo ld = new LatchDemo(latch);
        long start = System.currentTimeMillis();

        for (int i = 0; i < 10; i++) {
            new Thread(ld).start();
        }

        latch.await();
        long end = System.currentTimeMillis();

        System.out.println("总时长为:" + (end - start));
    }
}

class LatchDemo implements Runnable {

    private CountDownLatch latch;

    public LatchDemo(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            double random = Math.random() * 100;
            if (random > 99) {
                System.out.println(random);
            }
        }
        latch.countDown();
    }
}

Callable

Callable介绍

Runnable是执行工作的独立任务,但是它不返回任何值。在Java SE5中引入的Callable是一种具有类型参数的泛型,泛型类型是方法call()的返回的值类型。

四种执行线程方式的介绍

种数 种类 说明
1 实现Runnable接口 通过Thread实例启动它
2 继承Thread 重写Threadrun方法
3 实现Callable接口 通过FutureTask包装,然后再通过Thread启动
4 实现Callable接口 ExecutorServices.submit()

可参考博客:
彻底理解Java的Future模式
Future模式添加Callback及Promise 模式

Lock

用于解决多线程安全问题的方式:

  • synchronized:隐式锁、重量级

    1. 同步代码块
    2. 同步方法
  • jdk 1.5后,Lock:轻量级

    1. 同步锁Lock
      注意:是一个显示锁,需要通过lock()方法上锁,必须通过unlock()方法进行释放锁

多线程安全问题演示

买票案例代码演示:

public class Main {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(ticket,"一号售票窗口").start();
        new Thread(ticket,"二号售票窗口").start();
        new Thread(ticket,"三号售票窗口").start();
    }
}

class Ticket implements Runnable {

    private int num = 100;

    @Override
    public void run() {
        while (true) {
            if (num > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("卖出一张票,剩余还有:" + --num);
            }else if (num == 0){
                break;
            }
        }
    }
}

上面的代码存在线程安全问题 ,多线程下对同一共享变量进行修改。
用第一种保证安全性:

while (true) {
            synchronized (this){
                if (num > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("卖出一张票,剩余还有:" + --num);
                }else if (num == 0){
                    break;
                }
            }
        }

但是这样效率严重降低。
用第三种方式保证安全性:

while (true) {
            lock.lock();
            try{
                if (num > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("卖出一张票,剩余还有:" + --num);
                } else if (num == 0) {
                    break;
                }
            }finally {
                lock.unlock();
            }
        }

Condition

编写一个程序,开启3个线程,这三个线程的ID分别为A、B、C,每个线程将自己的ID在屏幕上打印10遍,要求输出的结果必须按顺序显示。
如:ABCABCABC….依次递归

public class Main {
    public static void main(String[] args) {
        Test test = new Test();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                test.LoopA();
            }
        },"A").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                test.LoopB();
            }
        },"B").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                test.LoopC();
            }
        },"C").start();
    }
}

class Test {

    private int id = 1;
    Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();

    public void LoopA() {
        lock.lock();
        try {
            while (id != 1) {
                condition1.await();
            }
            System.out.println("A");
            id = 2;
            condition2.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void LoopB() {
        lock.lock();
        try {
            while (id != 2) {
                condition2.await();
            }
            System.out.println("B");
            id = 3;
            condition3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void LoopC() {
        lock.lock();
        try {
            while (id != 3) {
                condition3.await();
            }
            System.out.println("C");
            id = 1;
            condition1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

ReadWriteLock 读写锁

写写/读写 需要“互斥”
读读 不需要互斥

public class Main {
    public static void main(String[] args) {
        Test test = new Test();
        for (int i = 0; i < 10; i++) {
            int j = i;
            new Thread(() -> {
                test.write("" + j, new Random().nextInt(10));
            }).start();
        }
        for (int i = 0; i < 100; i++) {
            int j = i;
            new Thread(() -> {
                test.read("" + j);
            }).start();
        }
    }
}

class Test {

    private int id = 1;
    private ReadWriteLock rwl = new ReentrantReadWriteLock();

    public void read(String name) {
        rwl.readLock().lock();
        try {
            System.out.println(String.format("名字:%s,读出的数据:%d", name, id));
        } finally {
            rwl.readLock().unlock();
        }
    }

    public void write(String name, int id) {
        rwl.writeLock().lock();
        try {
            this.id = id;
            System.out.println(String.format("我是写锁,名字:%s,改写数据为:%d", name, id));
        } finally {
            rwl.writeLock().unlock();
        }
    }
}

线程八锁

  1. 两个普通同步方法,两个线程,标准打印,打印?//one two
  2. 新增 Thread.sleep()给getone(),打印?//one two
  3. 新增普通方法 getThree(),打印?//three one two
  4. 两个普通同步方法,两个Number对象,打印?//two one
  5. 修改 getone()为静态同步方法,打印?//two one
  6. 修改两个方法均为静态同步方法,一个Number对象?//one two
  7. 一个静态同步方法,一个非静态同步方法,两个Number对象?//two one
  8. 两个静态同步方法,两个Number对象?//one two

线程八锁的关键:

  • 非静态方法的锁默认为this,静态方法的锁为对应的Class实例
  • 某一个时刻内,只能有一个线程持有锁,无论几个方法。

线程池

线程池介绍

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

线程池的体系结构:

java.util.concurrent.Executor:负责线程的使用与调度的根接口

  |--**ExecutorService子接口:线程池的主要接口
        |--ThreadPoolExecutor 线程池的实现类
        |--ScheduledExecutorService 子接口:负责线程的调度
              |--ScheduledThreadPoolExecutor:继承 ThreadPoolExecutor,
                                                实现 ScheduledExecutorService

工具类:Executors

ExecutorService newFixedThreadPool():创建固定大小的线程池
ExecutorService newCachedThreadPool():缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量。
ExecutorService newSingleThreadExecutor():创建单个线程池。线程池中只有一个线程
ScheduledExecutorService newScheduledThreadPool():创建固定大小的线程,可以狂迟或定时的执行任务。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,699评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,124评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,127评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,342评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,356评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,057评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,654评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,572评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,095评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,205评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,343评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,015评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,704评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,196评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,320评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,690评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,348评论 2 358

推荐阅读更多精彩内容