多线程

1、进程和线程

进程是资源分配的基本单位,可以看作是一个正在运行的程序,进程是动态的,程序是静态的。

线程是处理机调用和执行的基本单位。

  1. 线程拥有独立的运行栈和程序计数器。
  2. 一个进程中可以拥有有多个线程。
  3. 若一个进程同一时间并行执行多个线程,就是支持多线程
  4. **一个进程中的多个线程共享相同的内存单元/内存地址空间它们从同一堆中分配对象,可以访问相同的变量和对象。**这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。

在这里插入图片描述

一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

线程的分类:

  1. 守护线程
  2. 用户线程

在strat()方法前调用thread.setDaemo(true)可以把一个用户线程变成守护线程。

若JVM中都是守护线程,则当前JVM将退出。

并行与并发

并行:多个CPU同时执行多个任务。

并发:一个CPU(采用时间片)同时执行多个任务。轮换执行,只不过时间片小,轮换的速度快,看起来像是多个进程同时执行。

2、线程的创建和使用

JAVA提供了多线程的类。-------java.lang.Thread类

Thread类的构造器:

Thread()创建新的Thread对象
Thread(String threadName)创建线程并指明线程的实例名
Thread(Runnable target)指定创建线程的目标对象,它实现了Runnable接口中的run方法
Thread(Runnable target, String name)创建新的Thread对象

1、线程创建的方式

1、继承Thread类

    1. 创建一个继承于Thread类的子类
    1. 重写Thread类的run() --> 将此线程执行的操作声明在run()中 ------不能直接用 run(),直接调用run方法相当于没有启动新的线程,只是相当于main线程中跑了一遍重写的代码。
    1. 创建Thread类的子类的对象
    1. 通过此对象调用start()
class MyThread extends Thread {
    //2. 重写Thread类的run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        //3. 创建Thread类的子类的对象
        MyThread t1 = new MyThread();

        //4.通过此对象调用start():①启动当前线程 ② 调用当前线程的run()
        t1.start();
        //问题一:我们不能通过直接调用run()的方式启动线程。
//        t1.run();

 //问题二:再启动一个线程,遍历100以内的偶数。不可以还让已经start()的线程去执行。会报IllegalThreadStateException
//        t1.start();
        //我们需要重新创建一个线程的对象
        MyThread t2 = new MyThread();
        t2.start();
        //不能直接用 t2.run(),直接调用run方法相当于没有启动新的线程,只是相当于main线程中跑了一遍重写的代码。


        //如下操作仍然是在main线程中执行的。
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i + "***********main()************");
            }
        }
    }

}

在这里插入图片描述

注意点:

  1. 不能直接用 run(),直接调用run方法相当于没有启动新的线程,只是相当于main线程中跑了一遍重写的代码。
  2. 当线程已经strat后,不能在调用start(),会出现IllegalThreadStateException错误。线程的首次调用的时候threadStatus=0;
  3. 要想再启动一个线程,需要重新创建线程对象。
//匿名子类线程
new Thread(){
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);

            }
        }
    }
}.start(); 

2、实现Runnable接口

创建多线程的方式二:实现Runnable接口

//Runable接口源码

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface {@code Runnable} is used
     * to create a thread, starting the thread causes the object's
     * {@code run} method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method {@code run} is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}
    1. 创建一个实现了Runnable接口的类
    1. 实现类去实现Runnable中的抽象方法:run()
    1. 创建实现类的对象
    1. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
    1. 通过Thread类的对象调用start()
  • 开发中:优先选择:实现Runnable接口的方式

  • 原因:1. 实现的方式没有类的单继承性的局限性

    1. 实现的方式更适合来处理多个线程有共享数据的情况。
class MThread implements Runnable{

    //2. 实现类去实现Runnable中的抽象方法:run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }

        }
    }
}


public class ThreadTest1 {
    public static void main(String[] args) {
        //3. 创建实现类的对象
        MThread mThread = new MThread();
        //4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(mThread);
        t1.setName("线程1");
        //5. 通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()-->调用了Runnable类型的target的run()
        t1.start();

        //再启动一个线程,遍历100以内的偶数
        Thread t2 = new Thread(mThread);
        t2.setName("线程2");
        t2.start();
    }

}
//实现接口的run方法为什么会被Thread类对象的strat()方法执行
private Runnable target;

public Thread(Runnable target) {
    this(null, target, "Thread-" + nextThreadNum(), 0);
}

public void run() {
    if (target != null) {
        target.run();
    }
}

3、实现Callable接口

与使用Runnable相比, Callable功能更强大些(runnable重写run方法,Callable重写call方法)

  1.  相比run()方法,可以有返回值

  2.  方法可以抛出异常

  3.  支持泛型的返回值

  4.  需要借助FutureTask类,比如获取返回结果

    1. FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值在这里插入图片描述

      实现Future为了用他的get()方法获取返回值,实现Runnable是为了重写run()方法,在run()调用了call()方法,而call方法被我们实现Callable接口重写了,call方法的返回值可以用get()获取。本质还是实现runnable接口。

//Callable接口源码

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

调用方式和Runnable有点差异需要在借助FutureTask类。

将Callable接口的实现类对象装入FutureTask的构造器,返回FutureTask对象,将此对象装入Thread类构造器,返回线程,在start();

//1.创建一个实现Callable的实现类
class NumThread implements Callable<Integer>{ //支持泛型
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}


public class ThreadNew {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask<Integer> futureTask = new FutureTask(numThread);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();

        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
            Integer sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

4、使用线程池

好处:

1.提高响应速度(减少了创建新线程的时间)

2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)

3.便于线程管理(设置以下属性需要ThreadPoolExecutor对象)

​ corePoolSize:核心线程的大小

​ maximumPoolSize:最大线程数

​ keepAliveTime:临时线程的存货时间(多长时间不干活就会被销毁)

​ TimeUnit unit, // 临时线程存活的时间单位 (时分秒。。。)

​ BlockingQueue workQueue, // 阻塞队列

​ ThreadFactory threadFactory, // 创建线程方式

​ RejectedExecutionHandler handler) // 拒绝策略 , 四种
在这里插入图片描述

class NumberThread implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

public class ThreadPool {

    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
//        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //设置线程池的属性
//        System.out.println(service.getClass());
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();


        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable

//        service.submit(Callable callable);//适合使用于Callable
        //3.关闭连接池
        service.shutdown();
    }

}

自定义线程池:

package com.itheima.threadpool_demo;

import java.util.concurrent.*;

/*
    线程池 :
        1 Java提供好的线程池
        2 自定义线程池

   ThreadPoolExecutor类 :
   public ThreadPoolExecutor(int corePoolSize,  // 核心线程数量            3个核心线程
                          int maximumPoolSize,  // 池中允许的最大线程数    10,可以有7个临时线程
                          long keepAliveTime,   // 临时线程存活的时间      60
                          TimeUnit unit,        // 临时线程存活的时间单位   分
                          BlockingQueue<Runnable> workQueue,    // 阻塞队列  20
                          ThreadFactory threadFactory,          // 创建线程方式
                          RejectedExecutionHandler handler)     // 拒绝策略 , 四种

    饭馆 : 核心服务员3
    最多可以容纳10名服务员
 */
public class Test4 {
    int num = 10;
    public static void main(String[] args) {
        // ExecutorService threadPool2 = Executors.newFixedThreadPool(3);

        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                3,
                10,
                60,
                TimeUnit.MINUTES,
                new ArrayBlockingQueue<>(20),
                Executors.defaultThreadFactory(), //创建线程方式
                new ThreadPoolExecutor.AbortPolicy() //拒绝策略
        );

        for (int i = 0; i < 100; i++) {
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("自定义线程池执行的任务");
                }
            });
        }
    }
}

2、线程常用方法

void start()启动线程,并执行对象的run()方法
run()线程在被调度时执行的操作
String getName()返回线程的名称
void setName(String name)设置该线程名称
static Thread currentThread()返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
static void yield()线程让步 释放线程的执行权,把执行机会让给优先级相同或更高的线程 若队列中没有同优先级的线程,忽略此方法
join()当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止 低优先级的线程也可以获得执行
static void sleep(long millis)令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。 抛出InterruptedException异常
stop()强制线程生命期结束,不推荐使用
boolean isAlive()返回boolean,判断线程是否还活着

3、线程调度

调度策略:时间片轮转。

java调度方法

  • 同优先级组成队列(先到先服务)。
  • 高优先级使用抢占式策略。

3、1 线程优先级(Priority)

线程优先等级:

  1. MAX_PRIORITY: 10
  2. MIN _PRIORITY:1
  3. NORM_PRIORITY:5 默认优先级

涉及的方法:

  • getPriority() **:**返回线程优先值
  • setPriority(int newPriority) **:**改变线程的优先级。

注意点:

  1. 线程创建时继承父线程的优先级
  2. 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

4、线程的生命周期

public enum State {

        NEW,    //新建

        RUNNABLE,  //执行

        BLOCKED,  //阻塞

        WAITING,  //等待 wait()方法

        TIMED_WAITING,   //计时等待   sleep()方法 wait(int time)

        TERMINATED;  //消亡
    }

五状态模型:

  1. 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
  2. **就绪:**处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
  3. **运行:**当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
  4. **阻塞:**在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
  5. **死亡:**线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

在这里插入图片描述

5、线程同步

synchronized的锁是什么?

1、任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。  同步方法的锁:静态方法(类名.class)、非静态方法(this)  同步代码块:自己指定,很多时候也是指定为this或类名.class
2、 注意:
	2.1必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
	2.2一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)

1、同步代码块

synchronized(同步监视器){
     //需要被同步的代码
}

说明:

  • 1.操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
  •   2.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
    
  • 3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。要求:多个线程必须要共用同一把锁。
  •   补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
    
class Window1 implements Runnable{

    private int ticket = 100;
//    Object obj = new Object();
//    Dog dog = new Dog();
    @Override
    public void run() {
//        Object obj = new Object();
        while(true){
            synchronized (this){//此时的this:唯一的Window1的对象   //方式二:synchronized (dog) {

                if (ticket > 0) {

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

                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);


                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}


public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w = new Window1();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }

}


class Dog{

}

2、同步方法

class Window3 implements Runnable {

    private int ticket = 100;

    @Override
    public void run() {
        while (true) {

            show();
        }
    }

    private synchronized void show(){//同步监视器:this
        //synchronized (this){

            if (ticket > 0) {

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

                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);

                ticket--;
            }
        //}
    }
    
    private static synchronized void show(){//同步监视器:Window3.class
        //private synchronized void show(){ //同步监视器:t1,t2,t3。此种解决方式是错误的
        if (ticket > 0) {

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

            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
    
}

public class WindowTest3 {
    public static void main(String[] args) {
        Window3 w = new Window3();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

注意点:

  1. 同步代码块仍然有同步监视器,只是不需要我们显示声明。
  2. 非静态的方法的同步监视器是:this(当前对象)
  3. 静态方法的同步监视器是 类.class。

3、Lock锁(JDK5.0新增)

  1. 创建ReentrantLock对象实例,调用lock()和unlock()方法。
class Window implements Runnable{

    private int ticket = 100;
    //1.实例化ReentrantLock(可重入锁)
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true){
            try{

                //2.调用锁定方法lock()
                lock.lock();

                if(ticket > 0){

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

                    System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }finally {
                //3.调用解锁方法:unlock()
                lock.unlock();
            }

        }
    }
}
1、面试题:synchronized 与 Lock的异同?

相同:二者都可以解决线程安全问题

不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())

2.优先使用顺序:

Lock --> 同步代码块(已经进入了方法体,分配了相应资源) --> 同步方法(在方法体之外)

4、什么时候释放锁?

  1.  当前线程的同步方法、同步代码块执行结束。
  2.  当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
  3.  当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
  4.  当前线程在同步代码块、同步方法中执行了线程对象的**wait()**方法,当前线程暂停,并释放锁。

5、什么时候不会释放锁?

  1. 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
  2. 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。

6、线程安全的懒汉式单例模式

class Bank{

    private Bank(){}

    private static Bank instance = null;

    public static Bank getInstance(){
        //方式一:效率稍差
//        synchronized (Bank.class) {
//            if(instance == null){
//
//                instance = new Bank();
//            }
//            return instance;
//        }
        
        //方式二:效率更高
        if(instance == null){

            synchronized (Bank.class) {
                
                if(instance == null){
                    instance = new Bank();
                }
            }
        }
        return instance;
    }

}

6、线程通信

涉及到的三个方法:

  1. wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
  2. notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
  3. notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

说明:

  1. 1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
  2. 2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会IllegalMonitorStateException异常
  3. 3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
//线程通信的例子:使用两个线程打印 1-100。线程1, 线程2 交替打印

class Number implements Runnable{
    private int number = 1;
    private Object obj = new Object();
    @Override
    public void run() {

        while(true){

            synchronized (obj) {

                obj.notify();

                if(number <= 100){

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

                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;

                    try {
                        //使得调用如下wait()方法的线程进入阻塞状态
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }else{
                    break;
                }
            }

        }

    }
}


public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);

        t1.setName("线程1");
        t2.setName("线程2");

        t1.start();
        t2.start();
    }
}

面试题:sleep() 和 wait()的异同?

1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。

2.不同点:

1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()

2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中

3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

7、volatile关键字

1、JMM(java内存模型)

Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问(类似缓存机制)对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。

在这里插入图片描述

2、问题分析(并发编程下变量不可见性)

当多个线程都访问成员变量或类变量时,由于每个线程都会使用自己的工作内存,而自己的工作内存是重主存那里复制过来的,所以当一个线程修改变量值,然后更新主存的值,其他线程的工作内存并不知道变量已经修改,还是用的旧的值。这就造成了这种问题

//如下面的代码中的  flag变量  发生了这种问题。
public class VolatileThread extends Thread {

    // 定义成员变量
    private boolean flag = false ;
    public boolean isFlag() { return flag;}

    @Override
    public void run() {

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

        // 将flag的值更改为true
        this.flag = true ;
        System.out.println("flag=" + flag);

    }
}

public class VolatileThreadDemo {// 测试类
    
    public static void main(String[] args) {

        // 创建VolatileThread线程对象
        VolatileThread volatileThread = new VolatileThread() ;
        volatileThread.start();

        // main方法
        while(true) {
            if(volatileThread.isFlag()) {
                System.out.println("执行了======");
            }
        }
    }
}

3、解决方案

1、加锁

某一个线程进入synchronized代码块前后,执行过程入如下:

   a.线程获得锁

   b.清空工作内存

   c.从主内存拷贝共享变量最新的值到工作内存成为副本

   d.执行代码

   e.将修改后的副本的值刷新回主内存中

   f.线程释放锁

2、volatile关键字(及时更新)

使用volatile关键字:一旦变量值修改,会立即更新值。

   private volatile boolean flag ;

工作原理:在这里插入图片描述

  1. VolatileThread线程从主内存读取到数据放入其对应的工作内存

  2. 将flag的值更改为true,但是这个时候flag的值还没有写会主内存

  3. 此时main方法main方法读取到了flag的值为false

  4. 当VolatileThread线程将flag的值写回去后,失效其他线程对此变量副本

  5. 再次对flag进行操作的时候线程会从主内存读取最新的值,放入到工作内存中

    总结: volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。

  6. ​ 但是volatile不保证原子性。

3、volatile与synchronized

  • volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
  • volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。
  • volatile是通过 让修改过的值在其他线程的工作内存失效,再更新值 的方式保证了多线程下的数据的可见性 。
  • synchronized是通过 清除线程的工作内存,重新从主存中加载数据 的方式。

8、原子性(CAS乐观锁)

概述:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。(要么一气呵成,要么就放弃本次操作)

1、问题分析

public class VolatileAtomicThread implements Runnable {

    // 定义一个int类型的遍历
    private int count = 0 ;

    @Override
    public void run() {
        // 对该变量进行++操作,100次
        for(int x = 0 ; x < 100 ; x++) {
            count++ ;					
            System.out.println("count =========>>>> " + count);
        }
    }

}

public class VolatileAtomicThreadDemo {

    public static void main(String[] args) {

        // 创建VolatileAtomicThread对象
        VolatileAtomicThread volatileAtomicThread = new VolatileAtomicThread() ;

        // 开启100个线程对count进行++操作
        for(int x = 0 ; x < 100 ; x++) {
            new Thread(volatileAtomicThread).start();
        }
        
    }

}

在这里插入图片描述

本来三个线程应该可以把count加到53的,但是只加到51,原因是再执行count++这个语句不是原子性,再它执行的时后其他线程可以中断操作,count的更新值还没向主存中提交,另一个线程就已经读取了count值,这样一来,两个线程按原本的设想可以把count加到52的,结果只加到51。

2、 volatile原子性测试

小结:在多线程环境下,volatile关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性(在多线程环境下volatile修饰的变量也是线程不安全的)。

volatile的使用场景

  • 开关控制

    利用可见性特点,控制某一段代码执行或者关闭(比如今天课程的第一个案例)。

  • 多个线程操作共享变量,但是是有一个线程对其进行写操作,其他的线程都是读。

3、问题解决方式(锁机制,原子类)

1、锁

我们可以给count++操作添加锁,那么count++操作就是临界区的代码,临界区只能有一个线程去执行,所以count++就变成了原子操作。

2、原子类

AtomicInteger类(原子Integeter类)

public AtomicInteger():	   				初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer

int get():   			 				 获取值
int getAndIncrement():      			 以原子方式将当前值加1,注意,这里返回的是自增前的值。
int incrementAndGet():     				 以原子方式将当前值加1,注意,这里返回的是自增后的值。
int addAndGet(int data):				 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value):   			 以原子方式设置为newValue的值,并返回旧值。

案例改造:

public class VolatileAtomicThread implements Runnable {

    // 定义一个int类型的变量
    private AtomicInteger atomicInteger = new AtomicInteger() ;

    @Override
    public void run() {

        // 对该变量进行++操作,100次
        for(int x = 0 ; x < 100 ; x++) {
            int i = atomicInteger.getAndIncrement();
            System.out.println("count =========>>>> " + i);
        }
    }
}

4、原子类CAS机制(乐观锁机制)

AtomicInteger类底层使用CAS机制:在多线程的情况下,一个线程要想改变AtomicInteger对象的值,会把旧值和新值一起提交,然后对比内存中的旧值和提交的旧值,如果两个旧值相等,就把新值更新到内存中,如果不等说明其他线程已经把值给改了,则放弃本次操作。类似数据库的回滚操作。

CAS:CompareAndSwap 比较和交换,是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。CAS可以将read-modify-check-write转换为原子操作,这个原子操作直接由处理器保证。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

CAS与Synchronized:乐观锁与悲观锁

CAS和Synchronized都可以保证多线程环境下共享数据的安全性。那么他们两者有什么区别?

  1. Synchronized是从悲观的角度出发(悲观锁)

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。因此Synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁。性能较差!!

  1. CAS是从乐观的角度出发:

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。CAS这种机制我们也可以将其称之为乐观锁。综合性能较好!

9、并发包(Java.utill.Current下的东西)

1、ConcurrentHashMap

为什么要使用ConcurrentHashMap:

  1. HashMap线程不安全,会导致数据错乱
  2. 使用线程安全的Hashtable效率低下

HashTable与ConcurrentHashMap之间的区别:

  1. HashTable(锁数组)

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

在这里插入图片描述

  1. ConcurrentHashMap(锁桶)

ConcurrentHashMap高效的原因:CAS + 局部(synchronized)锁定分段式锁

在这里插入图片描述

小结:
    HashMap是线程不安全的。
    Hashtable线程安全基于synchronized,综合性能差,被淘汰了。
    ConcurrentHashMap:线程安全的,分段式锁,综合性能最好,线程安全开发中推荐使用

2、CountDownLatch(类似一个计数器)

1、使用场景:CountDownLatch允许一个或多个线程等待其他线程完成操作,再执行自己。

例如:线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后,要线程2打印B之后才能打印C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行。

2、方法与构造器

CountDownLatch构造方法:

public CountDownLatch(int count)// 初始化一个指定计数器的CountDownLatch对象

CountDownLatch重要方法:

public void await() throws InterruptedException// 让当前线程等待,必须down完初始化的数字才可以被唤醒,否则进入无限等待
public void countDown()	// 计数器进行减1

实例:

// 线程1
public class ThreadA extends Thread {
    private CountDownLatch down ;
    public ThreadA(CountDownLatch down) {
        this.down = down;
    }
    @Override
    public void run() {
        System.out.println("A");
        try {
            down.await(); //阻塞,当值为0的时候,不会再阻塞
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("C");
    }
}
//  线程二
public class ThreadB extends Thread {
    private CountDownLatch down ;
    public ThreadB(CountDownLatch down) {
        this.down = down;
    }
    @Override
    public void run() {
        System.out.println("B");
        down.countDown();
    }
}
// 测试类
public class Demo {
    public static void main(String[] args) {
        CountDownLatch down = new CountDownLatch(1);//创建1个计数器
        new ThreadA(down).start();
        new ThreadB(down).start();
    }
}

说明:

CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。

CountDownLatch是通过一个计数器来实现的,每当一个线程完成了自己的任务后,可以调用countDown()方法让计数器-1,当计数器到达0时,调用CountDownLatch的await()方法的线程阻塞状态解除,继续执行。

3、CyclicBarrier(可循环使用(Cyclic)的屏障(Barrier) )

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

在这里插入图片描述

每当线程调用await()时,就会进行加1,阻塞线程,当加到我们预定的值时,我们会把这几个线程全部释放。

CyclicBarrier构造方法:

public CyclicBarrier(int parties, Runnable barrierAction)// 用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景

CyclicBarrier重要方法:

public int await()// 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞

场景实例:公司召集5名员工开会,等5名员工都到了,会议开始。我们创建5个员工线程,1个开会线程,几乎同时启动,使用CyclicBarrier保证5名员工线程全部执行后,再执行开会线程。

  • 示例代码:
    1). 制作员工线程:
public class PersonThread extends Thread {
	private CyclicBarrier cbRef;
	public PersonThread(CyclicBarrier cbRef) {
		this.cbRef = cbRef;
	}
	@Override
	public void run() {
		try {
			Thread.sleep((int) (Math.random() * 1000));
			System.out.println(Thread.currentThread().getName() + " 到了! ");
			cbRef.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (BrokenBarrierException e) {
			e.printStackTrace();
		}
	}
}

2). 制作开会线程:

public class MeetingThread extends Thread {
    @Override
    public void run() {
        System.out.println("好了,人都到了,开始开会......");
    }
}

3). 制作测试类:

public class Demo {
	public static void main(String[] args) {
		CyclicBarrier cbRef = new CyclicBarrier(5, new MeetingThread());//等待5个线程执行完毕,再执行MeetingThread
		PersonThread p1 = new PersonThread(cbRef);
		PersonThread p2 = new PersonThread(cbRef);
		PersonThread p3 = new PersonThread(cbRef);
		PersonThread p4 = new PersonThread(cbRef);
		PersonThread p5 = new PersonThread(cbRef);
		p1.start();
		p2.start();
		p3.start();
		p4.start();
		p5.start();
	}
}

使用场景

使用场景:CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。

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

4、Semaphore(流量控制)

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

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

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

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

Semaphore构造方法:

public Semaphore(int permits)						permits 表示许可线程的数量
public Semaphore(int permits, boolean fair)			fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程

Semaphore重要方法:

public void acquire() throws InterruptedException	表示获取许可
public void release()								release() 表示释放许可
  • 示例一:同时允许2个线程同时执行
    1). 修改Service类,将new Semaphore(1)改为2即可:
public class Service {
    private Semaphore semaphore = new Semaphore(2);//2表示许可的意思,表示最多允许2个线程执行acquire()和release()之间的内容
    public void testMethod() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName()
                    + " 进入 时间=" + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName()
                    + "   结束 时间=" + System.currentTimeMillis());
            semaphore.release();
			//acquire()和release()方法之间的代码为"同步代码"
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2).线程类

public class ThreadA extends Thread {
	private Service service;
	public ThreadA(Service service) {
		super();
		this.service = service;
	}
	@Override
	public void run() {
		service.testMethod();
	}
}

3). 测试类

public class Demo {
	public static void main(String[] args) {
		Service service = new Service();
        //启动5个线程
		for (int i = 1; i <= 5; i++) {
			ThreadA a = new ThreadA(service);
			a.setName("线程 " + i);
			a.start();//5个线程会同时执行Service的testMethod方法,而某个时间段只能有2个线程执行
		}
	}
}

5、Exchanger(线程的数据交换)

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

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

Exchanger构造方法:

public Exchanger()

Exchanger重要方法:

public V exchange(V x)  // 具有阻塞作用
  • 示例二:exchange方法执行交换

1).制作线程A:

public class ThreadA extends Thread {
	private Exchanger<String> exchanger;
	public ThreadA(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		try {
			System.out.println("线程A欲传递值'礼物A'给线程B,并等待线程B的值...");
            
            // exchange()执行时会阻塞,直到收到另一个线程返回的值。
            // exchanger.exchange("礼物A")的返回值是 线程B exchange的东西
			System.out.println("在线程A中得到线程B的值=" + exchanger.exchange("礼物A"));
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

2).制作线程B:

public class ThreadB extends Thread {
	private Exchanger<String> exchanger;
	public ThreadB(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		try {
			System.out.println("线程B欲传递值'礼物B'给线程A,并等待线程A的值...");
			System.out.println("在线程B中得到线程A的值=" + exchanger.exchange("礼物B"));

		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

3).制作测试类:

public class Demo {
	public static void main(String[] args) throws InterruptedException {
		Exchanger<String> exchanger = new Exchanger<String>();
		ThreadA a = new ThreadA(exchanger);
		ThreadB b = new ThreadB(exchanger);
		a.start();
		b.start();
	}
}
  • 示例三:exchange方法的超时

1).制作线程A:

public class ThreadA extends Thread {
	private Exchanger<String> exchanger;
	public ThreadA(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		try {
			System.out.println("线程A欲传递值'礼物A'给线程B,并等待线程B的值,只等5秒...");
			System.out.println("在线程A中得到线程B的值 =" + exchanger.exchange("礼物A",5, TimeUnit.SECONDS));
			System.out.println("线程A结束!");
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (TimeoutException e) {
			System.out.println("5秒钟没等到线程B的值,线程A结束!");
		}
	}
}

2).制作测试类:

public class Run {
	public static void main(String[] args) {
		Exchanger<String> exchanger = new Exchanger<String>();
		ThreadA a = new ThreadA(exchanger);
		a.start();
	}
}
B extends Thread {
	private Exchanger<String> exchanger;
	public ThreadB(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		try {
			System.out.println("线程B欲传递值'礼物B'给线程A,并等待线程A的值...");
			System.out.println("在线程B中得到线程A的值=" + exchanger.exchange("礼物B"));

		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

3).制作测试类:

public class Demo {
	public static void main(String[] args) throws InterruptedException {
		Exchanger<String> exchanger = new Exchanger<String>();
		ThreadA a = new ThreadA(exchanger);
		ThreadB b = new ThreadB(exchanger);
		a.start();
		b.start();
	}
}
  • 示例三:exchange方法的超时

1).制作线程A:

public class ThreadA extends Thread {
	private Exchanger<String> exchanger;
	public ThreadA(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		try {
			System.out.println("线程A欲传递值'礼物A'给线程B,并等待线程B的值,只等5秒...");
			System.out.println("在线程A中得到线程B的值 =" + exchanger.exchange("礼物A",5, TimeUnit.SECONDS));
			System.out.println("线程A结束!");
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (TimeoutException e) {
			System.out.println("5秒钟没等到线程B的值,线程A结束!");
		}
	}
}

2).制作测试类:

public class Run {
	public static void main(String[] args) {
		Exchanger<String> exchanger = new Exchanger<String>();
		ThreadA a = new ThreadA(exchanger);
		a.start();
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值