java学习记录十八:线程二

一、线程安全

一、解释

由于java中是抢占式,有些时候不能按照我们规定的顺序来执行,会出现问题,因此需要解决。

二、 synchronized关键字

一、解释

  • synchronized关键字:表示“同步”的。它可以对“多行代码”进行“同步”——将多行代码当成是一个完整的整体,一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行。这样可以保证这多行的代码作为完整的整体,被一个线程完整的执行完毕。

  • synchronized被称为“重量级的锁”方式,也是“悲观锁”——效率比较低。

  • synchronized有几种使用方式:
    a).同步代码块【常用】
    b).同步方法【常用】

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。

二、同步代码块

同步代码块synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问,有人在访问这个代码块,其他人就不能再访问,必须等访问完。

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

三、同步锁

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

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

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

同步代码块中的同步锁
只要不变就行,要是this的话,要注意是什么

同步方法中的同步锁区分
​ 对于非static方法,同步锁就是this,谁调用这个对象
​ 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

四、写法

public class MyRunnable implements Runnable {
    // 共享变量
    int tickets = 100;

    @Override
    public void run() {
        // 线程的任务代码---卖票
        while (true) {
            // 加锁
            //this表示任务对象
            //或者写一个固定的字符串也行,即“world”,也行,只要所有的线程用同一个就行。
            synchronized (this) {
                if (tickets < 1) {
                    break;
                }
                // 暂停100ms模拟收钱的操作
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() +
                        ":正在出售第" + tickets + "张票");
                tickets--;
            }
            // 释放锁
            //四个窗口又开始抢钥匙,谁抢到就执行代码
        }
    }
}
public class Test {
    public static void main(String[] args) {
        // 创建任务对象
        MyRunnable mr = new MyRunnable();
        // 创建4个窗口---创建4条线程
        Thread t1 = new Thread(mr, "窗口1");
        Thread t2 = new Thread(mr, "窗口2");
        Thread t3 = new Thread(mr, "窗口3");
        Thread t4 = new Thread(mr, "窗口4");
        // 启动线程,执行任务
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

五、同步方法

ctrl+alt+f5变成同步方法
同步方法和js的节流类似,方法返回一个boolen值,判断方法使否在执行。
同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

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

六、写法

public class MyRunnable implements Runnable {
    // 共享变量
    int tickets = 100;

    @Override
    public void run() {
        // 线程的任务代码---卖票
        while (true) {
            if (sellTickets()) break;
        }
    }
    //将代码放到一个方法中,用synchronized 修饰,变成同步方法
    private synchronized boolean sellTickets() {
        if (tickets < 1) {
            return true;
        }
        // 暂停100ms模拟收钱的操作
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +
                ":正在出售第" + tickets + "张票");

        tickets--;
        return false;
    }
}

public class Test {
    public static void main(String[] args) {

        // 创建任务对象
        MyRunnable mr = new MyRunnable();

        // 创建4个窗口---创建4条线程
        Thread t1 = new Thread(mr, "窗口1");
        Thread t2 = new Thread(mr, "窗口2");
        Thread t3 = new Thread(mr, "窗口3");
        Thread t4 = new Thread(mr, "窗口4");

        // 启动线程,执行任务
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

七、解决一条线程使用的是同步代码块,一条线程使用的是同步方法出现的问题

开发中,一条线程使用的是同步代码块,一条线程使用的是同步方法,但这2条线程需要实现同步—>实现这个需求,同步代码块和同步方法的锁对象必须一致,而同步方法的锁对象是默认的,同步代码块的锁对象炫耀跟着同步代码块的锁对象走,所以必须清楚同步方法的锁对象。

public class Demo {
    // 锁对象: this--d这个对象
    public  synchronized void method1() {
        System.out.println(Thread.currentThread().getName() + ":打开厕所门...");
        System.out.println(Thread.currentThread().getName() + ":关闭厕所门...");
        System.out.println(Thread.currentThread().getName() + ":脱裤子...");
        System.out.println(Thread.currentThread().getName() + ":蹲下...");
        System.out.println(Thread.currentThread().getName() + ":用力...");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":擦屁股...");
        System.out.println(Thread.currentThread().getName() + ":穿裤子...");
        System.out.println(Thread.currentThread().getName() + ":冲厕所...");
        System.out.println(Thread.currentThread().getName() + ":打开厕所门,洗手,走人...");
    }

    // 锁对象: Demo.class
    public static synchronized void method2() {
        System.out.println(Thread.currentThread().getName() + ":打开厕所门...");
        System.out.println(Thread.currentThread().getName() + ":关闭厕所门...");
        System.out.println(Thread.currentThread().getName() + ":脱裤子...");
        System.out.println(Thread.currentThread().getName() + ":蹲下...");
        System.out.println(Thread.currentThread().getName() + ":用力...");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":擦屁股...");
        System.out.println(Thread.currentThread().getName() + ":穿裤子...");
        System.out.println(Thread.currentThread().getName() + ":冲厕所...");
        System.out.println(Thread.currentThread().getName() + ":打开厕所门,洗手,走人...");
    }
}

package com.itheima.demo4_同步方法的锁对象;
/**
 * @Author:pengzhilin
 * @Date: 2020/9/17 10:06
 */
public class Test {
    public static void main(String[] args) {
        // 上厕所不被打扰
        Demo d = new Demo();

        new Thread(new Runnable() {
            @Override
            public void run() {
                // 张三上厕所
                //这里锁用d对象
                synchronized (d) {
                    System.out.println(Thread.currentThread().getName() + ":打开厕所门...");
                    System.out.println(Thread.currentThread().getName() + ":关闭厕所门...");
                    System.out.println(Thread.currentThread().getName() + ":脱裤子...");
                    System.out.println(Thread.currentThread().getName() + ":蹲下...");
                    System.out.println(Thread.currentThread().getName() + ":用力...");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":擦屁股...");
                    System.out.println(Thread.currentThread().getName() + ":穿裤子...");
                    System.out.println(Thread.currentThread().getName() + ":冲厕所...");
                    System.out.println(Thread.currentThread().getName() + ":打开厕所门,洗手,走人...");
                }
            }
        }, "张三").start();

        // 李四上厕所---调用method1方法
        new Thread(new Runnable() {
            @Override
            //这里的锁是d对象
            public void run() {
                // 李四上厕所
                d.method1();
            }
        }, "李四").start();

    }
}

三、lock锁

一、解释

java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更加面向对象,可以在任意位置枷锁,synchronized是只能在固定位置加锁

二、写法

//public void lock() :加同步锁。
//`public void unlock():释放同步锁。
public class MyRunnable implements Runnable {
    // 共享变量
    int tickets = 100;
    //创建lock锁对象
    Lock lock = new ReentrantLock();
    @Override
    public void run() {
        // 线程的任务代码---卖票
        while (true) {
            // 加锁
            lock.lock();
            if (tickets < 1) {
            //这里一定也要释放锁
            //不然程序不会停止
                lock.unlock();
                break;
            }
            // 暂停100ms模拟收钱的操作
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() +
                    ":正在出售第" + tickets + "张票");

            tickets--;
            // 释放锁
            lock.unlock();
        }
    }
}


public class Test {

    public static void main(String[] args) {
        // 创建任务对象
        MyRunnable mr = new MyRunnable();
        // 创建4个窗口---创建4条线程
        Thread t1 = new Thread(mr, "窗口1");
        Thread t2 = new Thread(mr, "窗口2");
        Thread t3 = new Thread(mr, "窗口3");
        Thread t4 = new Thread(mr, "窗口4");
        // 启动线程,执行任务
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

使用如下:

四、高并发和线程安全

一、解释

  • 高并发:是指在某个时间点上,有大量的用户(线程)同时访问同一资源。例如:天猫的双11购物节、12306的在线购票在某个时间点上,都会面临大量用户同时抢购同一件商品/车票的情况。
  • 线程安全:在某个时间点上,当大量用户(线程)访问同一资源时,由于多线程运行机制的原因,可能会导致被访问的资源出现"数据污染"的问题。

五、多线程运行机制

一、解释

当一个线程启动后,JVM会为其分配一个独立的"线程栈区",这个线程会在这个独立的栈区中运行。
多个线程在各自栈区中独立、无序的运行,当访问一些代码,或者同一个变量时,就可能会产生一些问题
main方法是在主线程的栈区中,

六、多线程安全性问题

分为三个可见性,有序性,原子性

一、可见性问题

1.解释

概述: 一个线程没有看见另一个线程对共享变量的修改

2.例子

public class MyThread extends Thread {

    static boolean flag = false;// 主和子线程共享变量

    @Override
    public void run() {

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 把flag的值改为true
        flag = true;
        System.out.println("修改后flag的值为:"+flag);

    }
}

public class Test {
    public static void main(String[] args) {
        // 创建子线程并启动
        MyThread mt = new MyThread();
        mt.start();

        // 主线程
        while (true){
        //这里加一个耗时的操作,有可能会拿到true,但这是不可控制操作,不建议这么做
        // System.out.println("模拟耗时操作");
            if (MyThread.flag == true){
                System.out.println("死循环结束");
                break;
            }
        }
        /*
            按照分析结果应该是: 子线程把共享变量flag改为true,然后主线程的死循环就可以结束
            实际结果是: 子线程把共享变量flag改为true,但主线程依然是死循环
            其实原因就是子线程对共享变量flag修改后的值,对于主线程是不可见的
            由于死循环是非常简单,接近底层代码,所以只需速度非常的快,来不及去主内存中重新获取新的值,所以主线程工作内存中的flag的值一直是false,一直死循环,如果某一时刻,主线程去主内存中重新获取修改后的flag值,就会结束死循环,但主线程什么时候会去主内存中获取修改后的flag值,我们不确定,所以可能存在线程可见性问题.
         */

    }
}

3、分析

  • Java内存模型JMM(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
  • 简而言之: 就是所有共享变量都是存在主内存中的,线程在执行的时候,有单独的工作栈内存,会把共享变量拷贝一份到线程的单独工作内存中,并且对变量所有的操作,都是在单独的工作内存中完成的,不会直接读写主内存中的变量值

二、有序性问题

1.解释

线程执行顺序不固定

2.分析

  • 有些时候“编译器”在编译代码时,会对代码进行“重排”,例如:

    ​ int a = 10; //1

    ​ int b = 20; //2

    ​ int c = a + b; //3

    第一行和第二行可能会被“重排”:可能先编译第二行,再编译第一行,总之在执行第三行之前,会将1,2编译完毕。要是只有一个线程,1和2先编译谁,不影响第三行的结果。但在“多线程”情况下,代码重排,可能会对另一个线程访问的结果产生影响。

三、原子性问题

1.解释

所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。之前的卖票解决方式是加锁来解决原子性问题的。
2.例子
一条子线程和一条主线程都对共享变量a进行++操作,每条线程对a++操作100000次

public class MyThread extends Thread {
    // 共享变量
    static int a = 0;

    @Override
  public void run() {
        // 任务:对共享变量a自增10万次
      for (int i = 0; i < 100000; i++) {
            a++;
        }
        System.out.println("子线程执行完毕!");
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建线程对象
        MyThread mt = new MyThread();
        // 启动线程
        mt.start();
        // 主线程对共享变量a自增10万次
        for (int i = 0; i < 100000; i++) {
            MyThread.a++;
      }
        // 暂停,保证主线程和子线程都对共享变量a自增完了10万次,再统计a的结果
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 打印最终共享变量a的值
        System.out.println("最终:"+MyThread.a);// 最终:162243
        /*
            期望:最终a的值为200000
         */
    }
}

3.分析

两个线程对共享变量的操作产生覆盖的效果

七、解决线程安全性问题

一、volatile关键字

1.解释

  • volatile是一个"变量修饰符",它只能修饰"成员变量",它能强制线程每次从主内存获取值,并能保证此变量不会被编译器优化。

  • volatile能解决变量的可见性、有序性;

  • volatile不能解决变量的原子性
    二、写法

  • 解决可见性
    直接在共享变量前面加上volatile即可。出现可见性的原因就是线程不会每次从主内存中重新获取值,volatile关键字就是强制线程每次去重新获取值。

  • 解决有序性
    在不要重排的变量前面加上volatile即可。可以防止代码重排

  • 不能解决原子性
    上面两个只是获取,而原子性还会操作共享变量,主要问题是不知道把多个线程的哪个值赋值给共享变量,所以会出现覆盖问题,不是重新获取的问题。

二、原子类

一、解释
解决共享变量原子性问题,也能解决有序性和可见性问题。同步锁和lock锁也能解决原子性问题,但是有点大材小用。
在java.util.concurrent.atomic包下定义了一些对“变量”操作的“原子类”:

​ 1).java.util.concurrent.atomic.AtomicInteger:对int变量操作的“原子类”;

​ 2).java.util.concurrent.atomic.AtomicLong:对long变量操作的“原子类”;

​ 3).java.util.concurrent.atomic.AtomicBoolean:对boolean变量操作的“原子类”;
它们可以保证对“变量”操作的:原子性、有序性、可见性。

二、AtomicInteger类

public class MyThread extends Thread {
    // 共享变量
    //static int a = 0;
    //static AtomicInteger a = new AtomicInteger();// a表示整数0
    static AtomicInteger a = new AtomicInteger(0);// a表示整数0

    @Override
    public void run() {
        // 任务:对共享变量a自增10万次
      for (int i = 0; i < 100000; i++) {
            //a++;
          a.getAndIncrement();// 相当于a++
        }
        System.out.println("子线程执行完毕!");
    }
}
public class Test {
  public static void main(String[] args) {
        // 创建线程对象
        MyThread mt = new MyThread();
        // 启动线程
        mt.start();
        // 主线程对共享变量a自增10万次
        for (int i = 0; i < 100000; i++) {
            //MyThread.a++;
            MyThread.a.getAndIncrement();
        }
        // 暂停,保证主线程和子线程都对共享变量a自增完了10万次,再统计a的结果
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 打印最终共享变量a的值
        System.out.println("最终:"+ MyThread.a);

    }
}

工作原理:
cas机制:比较并交换
1.从主内存中获取共享变量的值
2。拿从内存中获取的共享变量的值与当前主内存中的值进行比较
3.如果相同,就自增1,写回主内存如果不相同,就重新获取,重新判断,重新自增,重新写回主内存

三、AtomicIntegerArray类
常用的数组操作的原子类:
1).java.util.concurrent.atomic.AtomicIntegetArray:对int数组操作的原子类。 int[]

​ 2).java.util.concurrent.atomic.AtomicLongArray:对long数组操作的原子类。long[]

​ 3).java.util.concurrent.atomic.AtomicReferenceArray:对引用类型数组操作的原子类。Object[]

public class MyThread extends Thread {
     //使用这个会出现有的不是1000
    // public static int[] arr = new int[1000];
    //改用原子类,使用数组构造
    public static AtomicIntegerArray arr = new AtomicIntegerArray(1000);
    @Override
    public void run() {
        for (int i = 0; i < arr.length(); i++) {
            arr.addAndGet(i, 1);//将i位置上的元素 + 1
        }
        System.out.println("结束");
    }
}
 public class Demo {
      public static void main(String[] args) throws InterruptedException {
          for (int i = 0; i < 1000; i++) {
              new MyThread().start();
          }
          Thread.sleep(1000 * 5);//让所有线程执行完毕
  
          System.out.println("主线程休息5秒醒来");
          for (int i = 0; i < MyThread.arr.length(); i++) {
              System.out.println(MyThread.arr.get(i));
          }
      }
  }

八、并发包

一、解释

在JDK的并发包里提供了几个非常有用的并发容器和并发工具类。供我们在多线程开发中进行使用。

二、CopyOnWriteArrayList

1.ArrayList的线程不安全演示

public class MyThread1 extends Thread{
    // 共享变量
    static ArrayList<Integer> list = new ArrayList<>();

    @Override
    public void run() {
        // 往集合中添加100000个元素
        for (int i = 0; i < 100000; i++) {
            list.add(i);
        }
    }
}
public class Test1 {
    public static void main(String[] args) {
        // 演示ArrayList线程不安全:2条线程都往ArrayList集合中添加100000个元素
        // 创建并启动线程
        new MyThread1().start();

        // 主线程往集合中添加100000个元素
        for (int i = 0; i < 100000; i++) {
            MyThread1.list.add(i);z
        }

        // 为了保证主线程和子线程对list集合都操作完毕
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("list集合元素个数:"+ MyThread1.list.size());
        /*
            期望: 200000个元素
            实际: 少于200000个元素
         */
    }
}
//最终结果可能会抛异常,或者最终集合大小是不正确的。

2.CopyOnWriteArrayList是线程安全的

 // 共享变量
    //static ArrayList<Integer> list = new ArrayList<>();
 // 把共享变量变成并发包即可,就能正确的添加元素
    static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();

三、CopyOnWriteArraySet

1.演示HashSet线程不安全

public class MyThread extends Thread {

	public static Set<Integer> set = new HashSet<>();//线程不安全的


    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            set.add(i);
        }
        System.out.println("添加完毕!");
    }
}

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1 = new MyThread();
        t1.start();
        //主线程也添加10000个
        for (int i = 10000; i < 20000; i++) {
            MyThread.set.add(i);
        }
        Thread.sleep(1000 * 3);
        System.out.println("最终集合的长度:" + MyThread.set.size());
    }
}
//最终结果可能会抛异常,也可能最终的长度是错误的!!

2.CopyOnWriteArraySet是线程安全的

   //改用:线程安全的Set集合:
    public static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();

四、ConcurrentHashMap

1.演示HashMap线程不安全

public class MyThread extends Thread {
    // 共享变量
    static HashMap<Integer, Integer> map = new HashMap<>();

    @Override
    public void run() {
        // 往集合中添加键值对
        for (int i = 0; i < 300000; i++) {
            map.put(i,i);
        }
    }
}
public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        // 演示HashMap线程不安全:2条线程往集合中添加键值对
        new MyThread().start();

        // 往集合中添加键值对
        for (int i = 0; i < 300000; i++) {
            MyThread.map.put(i,i);
        }

        Thread.sleep(5000);

        System.out.println("集合键值对个数:"+MyThread.map.size());
        /*
            期望: 3万个
            实际: 大于3万个
         */

    }
}
//运行结果可能会出现异常、或者结果不准确!!

2.Hashtable是线程安全的,但效率低:

static Hashtable<Integer, Integer> map = new Hashtable<>();
public synchronized V put(K key, V value) 
public synchronized V get(Object key)

HashTable容器使用synchronized同步锁来保证线程安全,锁定整个哈希表但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。
3.ConcurrentHashMap线程安全

static ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>();

ConcurrentHashMap是锁定局部的,CAS+局部synchronized 锁定

五、CountDownLatch(并发工具类)

一、解释

CountDownLatch允许一个或多个线程等待其他线程完成操作。例如:线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后,要线程2打印B之后才能打印C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行。

二、写法

//CountDownLatch构造方法:
public CountDownLatch(int count)// 初始化一个指定计数器的CountDownLatch对象
//CountDownLatch重要方法:
public void await() throws InterruptedException// 让当前线程等待
public void countDown()	// 计数器进行减1

public class MyThread1 extends Thread {


    CountDownLatch cdl;
    public MyThread1(CountDownLatch cdl) {
        this.cdl = cdl;
    }
    @Override
    public void run() {
        //打印A
        System.out.println("打印A...");

        //调用await()方法进入等待(线程2打印B)
        try {
            cdl.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //打印C
        System.out.println("打印C...");
    }
}
public class MyThread2 extends Thread {

    CountDownLatch cdl;

    public MyThread2(CountDownLatch cdl) {
        this.cdl = cdl;
    }

    @Override
    public void run() {
        //  打印B
        System.out.println("打印B...");

        //  调用countDown()方法让计数器-1
        cdl.countDown();
    }
}
public class Test {
    public static void main(String[] args) throws InterruptedException {
        /*
            例如:线程1要执行打印:A和C,线程2要执行打印:B,但要求线程1打印C之前,一定要打印B
            分析:
                线程1:
                    任务:
                        打印A
                        调用await()方法进入等待(线程2打印B)
                        打印C
                线程2:
                    任务:
                        打印B
                        调用countDown()方法让计数器-1

                注意:
                    1.创建的CountDownLatch对象的计数器初始值为1
                    2.线程1和线程2使用的CountDownLatch对象要一致
         */
        // 创建CountDownLatch对象,指定计数器的值为1
        CountDownLatch cdl = new CountDownLatch(1);
        // 创建并启动线程
        new MyThread1(cdl).start();
        Thread.sleep(5000);
        new MyThread2(cdl).start();
    }
}

六、CyclicBarrier

一、解释

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
例如:公司召集5名员工开会,等5名员工都到了,会议开始。

我们创建5个员工线程,1个开会线程,几乎同时启动,使用CyclicBarrier保证5名员工线程全部执行后,再执行开会线程。
使用场景:CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。

需求:使用两个线程读取2个文件中的数据,当两个文件中的数据都读取完毕以后,进行数据的汇总操作。

二、写法

//CyclicBarrier构造方法:
public CyclicBarrier(int parties, Runnable barrierAction
    //parties: 代表要达到屏障的线程数量,写了多少个,就要有多少线程到达屏障点才会执行
    
    //barrierAction:表示达到屏障后要执行的线程
//CyclicBarrier重要方法:
public int await()// 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
public class MyRunnable implements Runnable {

    CyclicBarrier cb;

    public MyRunnable(CyclicBarrier cb) {
        this.cb = cb;
    }
    @Override
    public void run() {
        //  到达会议室
        System.out.println(Thread.currentThread().getName()+":到达了会议室");
        //调用await()方法告诉CyclicBarrier,当前线程到了屏障,然后当前线程阻塞
        try {
            cb.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
        //离开会议室
        System.out.println(Thread.currentThread().getName()+":离开会议室");

    }
}
public class Test {
    public static void main(String[] args) {
        /*      
            分析:
                1.5名员工就可以使用5条线程来表示
                2.5条线程的任务都一样:
                    到达会议室
                    调用await()方法告诉CyclicBarrier,当前线程到了屏障,然后当前线程阻塞
                    离开会议室
         */
        // 创建CyclicBarrier
        //
        CyclicBarrier cb = new CyclicBarrier(5, new Runnable() {
            @Override
            public void run() {
                System.out.println("好了,人到齐了,咱们开始开会...");
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("好了,咱们今天的会议就到这结束,晚上聚餐!");
            }
        });
        // 创建任务
        MyRunnable mr = new MyRunnable(cb);
        // 创建5条线程
        new Thread(mr,"员工1").start();
        new Thread(mr,"员工2").start();
        new Thread(mr,"员工3").start();
        new Thread(mr,"员工4").start();
        new Thread(mr,"员工5").start();

    }
}

七、Semaphore

一、解释

Semaphore的主要作用是控制线程的并发数量。

synchronized可以起到"锁"的作用,但某个时间段内,只能有一个线程允许执行。

Semaphore可以设置同时允许几个线程执行。

Semaphore字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目。

二、写法

//Semaphore构造方法:
public Semaphore(int permits)						permits 表示许可线程的数量
//Semaphore重要方法:
public void acquire() throws InterruptedException	表示获取许可
public void release()								release() 表示释放许可

示例:同时允许1个线程执行

public class ClassRoom {

    Semaphore sp = new Semaphore(3);

    public void comeIn(){
        // 获得许可
        try {
            sp.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 在教室
        System.out.println(Thread.currentThread().getName()+":获得许可,进入教室...");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":离开教室,释放许可...");
        // 释放许可
        sp.release();
    }
}
public class Test {
    public static void main(String[] args) {
        /*
               演示:5名同学要进教室,但要设置每次只能2个同学进入教室
         */
        // 创建ClassRoom对象
        ClassRoom cr = new ClassRoom();
        // 创建并启动线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                cr.comeIn();
            }
        }, "张三1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                cr.comeIn();
            }
        }, "张三2").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                cr.comeIn();
            }
        }, "张三3").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                cr.comeIn();
            }
        }, "张三4").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                cr.comeIn();
            }
        }, "张三5").start();

    }
}

八、Exchanger

一、解释

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。

这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange()方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

A线程 exchange方法 把数据传递B线程

B线程 exchange方法 把数据传递A线程

使用场景:可以做数据校对工作

需求:比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水。为了避免错误,采用AB岗两人进行录入,录入到两个文件中,系统需要加载这两个文件,

并对两个文件数据进行校对,看看是否录入一致,

二、写法

//Exchanger构造方法:
public Exchanger()
//Exchanger重要方法:
public V exchange(V x)
public class MyThread1 extends Thread {

    Exchanger<String> ex;

    public MyThread1(Exchanger<String> ex) {
        this.ex = ex;
    }

    @Override
    public void run() {
        System.out.println("线程1:准备把数据传递给线程2...");
        String msg = null;
        try {
            msg = ex.exchange("数据1");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程1: 接收到线程2的数据是"+msg);
    }
}

public class MyThread2 extends Thread {

    Exchanger<String> ex;

    public MyThread2(Exchanger<String> ex) {
        this.ex = ex;
    }

    @Override
    public void run() {
        System.out.println("线程2:准备把数据传递给线程1...");
        String msg = null;
        try {
            msg = ex.exchange("数据2");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程2: 接收到线程1的数据是"+msg);
    }
}
public class Test {
    public static void main(String[] args) {
        /*
                演示:
                    线程1: 把"数据1"传递给线程2
                    线程2: 把"数据2"传递给线程1
         */
        // 创建Exchanger对象
        Exchanger<String> ex = new Exchanger<>();
        // 创建并启动线程
        new MyThread1(ex).start();
        new MyThread2(ex).start();

    }
}

九、线程池

一、解释

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?在Java中可以通过线程池来达到这样的效果。
线程池: 其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

二、线程池工作原理

工作线程:线程池中的线程,在没有任务时处于等待状态,可以循环的执行任务
任务队列:用于存放没有处理的任务。提供一种缓冲机制
任务接口:每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等
线程池管理器:用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务
创建线程池的时候,初始化指定数量的线程,当有任务需要线程执行的时候,就在线程池中随机分配空闲线程来执行当前的任务,如果线程池中没有空闲的线程,那么该任务就进入任务队列中进行等待,等待其他线程空闲下来,再执行任务(线程重复利用)

三、线程池的好处

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

十、线程池的使用

一、解释

Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工厂类来创建线程池对象。
Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行任务

  • public <T> Future<T> submit(Callable<T> task):获取线程池中的某一个线程对象,并执行任务

    Future接口:用来封装记录线程任务执行完毕后产生的结果。

使用线程池中线程对象的步骤:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)
  4. 关闭线程池(一般不做)。

二、写法一

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        //任务
        System.out.println(Thread.currentThread().getName()+":开始执行实现Runnable方式的任务....");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":执行完毕....");
    }
}
public class Test1_Runnable {
    public static void main(String[] args) {
        /*
            线程池使用一:任务是通过实现Runnable的方式创建
              1.使用Executors工厂类中的静态方法来创建线程池:
                    public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象,通过参数指定线程池中的线程数量
              2..提交并执行任务:
                    - public Future<?> submit(Runnable task):通过参数传入任务,获取线程池中的某一个线程对象,并执行任务
         */
        // 1.创建线程池,初始化线程
        ExecutorService es = Executors.newFixedThreadPool(3);// 创建一个线程池对象,该线程池中有3条线程

        // 2.创建任务
        MyRunnable mr = new MyRunnable();

        // 3.提交并执行任务
        es.submit(mr);
        es.submit(mr);
        es.submit(mr);
        es.submit(mr);

        // 4.销毁线程池(一般不操作)
        //es.shutdown();
    }
}

三、写法二

Callable测试代码:

  • <T> Future<T> submit(Callable<T> task) : 获取线程池中的某一个线程对象,并执行.

    Future : 表示计算的结果.

  • V get() : 获取计算完成的结果。

- public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("任务开始...");
        Thread.sleep(5000);
        System.out.println("任务结束...");
        return "结果";
    }
}


public class Test2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 1.创建线程池,初始化2条线程
        ExecutorService pools = Executors.newFixedThreadPool(2);

        // 2.创建任务
        MyCallable mc = new MyCallable();

        // 3.提交任务,执行任务
        pools.submit(mc);
        pools.submit(mc);
        pools.submit(mc);
        pools.submit(mc);
        pools.submit(mc);
        pools.submit(mc);
        pools.submit(mc);
        Future<String> f = pools.submit(mc);
        System.out.println(f.get());// itheima

        // 4.销毁线程池(开发中,一般不会)
        pools.shutdown();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值