多线程笔记

2019-06-24  本文已影响0人  small瓜瓜

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同时有了两个值

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

为了解决共享变量的不一致性,使得多线程对共享变量的修改的可见。上述结果我们可以使用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包

可参考博客:
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

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

多线程安全问题演示

买票案例代码演示:

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

线程八锁的关键:

线程池

线程池介绍

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

线程池的体系结构:

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

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

工具类:Executors

ExecutorService newFixedThreadPool():创建固定大小的线程池
ExecutorService newCachedThreadPool():缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量。
ExecutorService newSingleThreadExecutor():创建单个线程池。线程池中只有一个线程
ScheduledExecutorService newScheduledThreadPool():创建固定大小的线程,可以狂迟或定时的执行任务。
上一篇下一篇

猜你喜欢

热点阅读