java多线程

多线程前言

并行与并发

从操作系统的角度来看,线程是CPU分配的最小单位。

  • 并行就是同一时刻,两个线程都在执行。这就要求有两个CPU去分别执行两个线程。
    如多个任务在多个 CPU 或 CPU 的多个核上同时执行,不存在 CPU 资源的竞争、等待行为。

  • 并发就是同一时刻,只有一个执行,但是一个时间段内,两个线程都执行了。并发的实现依赖于CPU切换线程,因为切换的时间特别短,所以基本对于用户是无感知的。

在多线程编程实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。

package com.demo.juc.demo;

public class Test1 {

    public static void main(String[] args) {

        // 获取cpu的核数 16
        // 一个CPU可以有多个内核,内核就是真正的物理核心,而往往处理器会使用超线程技术,其将每个内核又可以分为两个线程,
        // 而线程技术就是在单个内核基础上提供两个逻辑处理器,利用特殊的硬件指令,把两个逻辑内核模拟成两个物理芯片,
        // 所以两个物理核心就是四个线程,也就形成了四个逻辑处理器。
        // 现在一般说多少核一般都是指有多少逻辑处理器。
        System.out.println(Runtime.getRuntime().availableProcessors());
    }
}

并发编程的本质:充分利用cpu的资源。

进程与线程

  • 进程

在操作系统中运行的程序就是进程,比如qq,播放器,游戏,idea等等

一个进程可以有多个线程,如视频中同时听声音,看图像,看弹幕等等

进程是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;
进程也是程序的一次执行过程,是系统运行程序的基本单位;
系统运行一个程序即是一个进程从创建、运行到消亡的过程。
简单说,进程就是正在执行的一个程序

  • 线程

线程是cpu调度和执行的单位

线程:进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简单说,线程就是进程中的多个执行功能,并拥有独立的执行路径

在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统密切相关的,先后顺序是不能人为干预的。

注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。

  • 其他
  1. 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
  2. 线程会带来额外的开销,如cpu调度时间,并发控制开销
  3. 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

java默认有两个线程,main线程和gc线程。

java真的可以开启线程么?

// Thread.start() 开启线程的源码
// 本地方法,调用底层的c++,java无法直接操作硬件
private native void start0();

Thread类 用来描述线程

构造方法

// 分配一个新的线程对象。
public Thread()
// 分配一个指定名字的新的线程对象。
public Thread(String name)
// 分配一个带有指定目标新的线程对象。
public Thread(Runnable target) 
// 分配一个带有指定目标新的线程对象并指定名字。
public Thread(Runnable target,String name)

常用方法

// 设置线程名称
void setName(String name) 
// 获取当前线程名称。
public String getName()
// 启动一个新的线程
public void start()
// 此线程要执行的任务在此处定义代码。
public void run()
// 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
public static void sleep(long millis)
// 返回对当前正在执行的线程对象的引用。
public static Thread currentThread()
// 更改线程的优先级
public final void setPriority(int newPriority)
// 暂停当前正在执行的线程对象,并执行其他线程
public static native void yield();
// 别用这个方式,中断线程
public void interrupt()
// 测试线程是否处于活动状态
public final native boolean isAlive();
// 等待该线程终止,插队
public final synchronized void join(long millis)




// run()和start()的区别
// start() : 它的作用是启动一个新线程,新线程会执行相应的run()方法。start()不能被重复调用。
		
// run()就和普通的成员方法一样,可以被重复调用。单独调用run()的话,会在当前线程中执行run(),而并不会启动新线程!

停止线程

  • 不推荐使用jdk提供的stop(),destroy()方法。【已废弃】
  • 推荐线程自己停止下来
  • 建议使用一个标志位进行终止变量,
    当flag=false,则终止线程运行。

stop()不安全,不建议使用:

  • 调用 stop() 方法会立刻停止 run() 方法中剩余的全部任务,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常,因此可能会导致任务执行失败。

  • 调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用 thread.stop() 后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用stop方法来终止线程。

package com.demo.threadDemo;


// 测试停止线程
public class StopThreadDemo implements Runnable {

    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        while (flag){
            System.out.println("run thread"+i++);
        }
    }


    // 转换标志位
    public void stop(){
        this.flag = false;
    }


    public static void main(String[] args) throws InterruptedException {
        StopThreadDemo stopThreadDemo = new StopThreadDemo();

        new Thread(stopThreadDemo).start();

        for (int i=0;i<2;i++){

            // 这儿睡眠1ms的原因是main线程太快的话,上面的线程还没执行,flag就已经为false了,
            // 就失去测试的意义了
            Thread.sleep(1);
            if (i==1){
                stopThreadDemo.stop();
                System.out.println("线程停止");
            }

        }

    }
}

线程休眠 sleep()

  • sleep(时间)指定当前线程阻塞的毫秒数
  • sleep存在异常InterruptedException
  • sleep时间达到后线程就进入就绪状态
  • sleep可以模拟网络延时,倒计时等
  • 每一个对象都有一个锁,sleep不会释放锁

线程礼让 yield()

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让cpu重新调度,礼让不一定成功!看cpu心情

a线程进入cpu正在执行,a礼让,退出正在执行,此时a线程与b线程共同竞争cpu资源,有可能a再次进入cpu执行,也有可能b进入cpu开始执行。

package com.demo.threadDemo;

public class TestYield {

    public static void main(String[] args) {

        MyYield myYield = new MyYield();
        new Thread(myYield,"a").start();
        new Thread(myYield,"b").start();

        // 礼让成功,如果没有礼让,那b线程就开始执行、b线程停止执行就结束了
        // b线程开始执行
        //a线程开始执行
        //b线程停止执行
        //a线程停止执行



        // 礼让不成功
        //a线程开始执行
        //a线程停止执行
        //b线程开始执行
        //b线程停止执行
    }
}


class MyYield implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程开始执行");
        Thread.yield();//礼让
        System.out.println(Thread.currentThread().getName() + "线程停止执行");
    }
}

join() 插队

join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞

package com.demo.threadDemo;

public class TestJoin implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {

            System.out.println("线程vip来了"+i);
        }
    }

    public static void main(String[] args) throws InterruptedException {

        // 启动我们的线程
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
        thread.start();

        // 主线程
        for (int i = 0; i < 100; i++) {

            if (i==20){
                // 我们的线程强行插队,直至执行完毕
                // 主线程进入阻塞
                thread.join();
            }
            System.out.println("main"+i);
        }





    }
}

观测线程状态 getState()

package com.demo.threadDemo;

// 观察线程的状态
public class TestState {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 5; i++) {

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("///");
        });

        // 观察状态
        // NEW 新建
        Thread.State state = thread.getState();
        System.out.println(state);

        // 观察启动后
        thread.start(); // 启动线程
        state = thread.getState();
        System.out.println(state); // RUNNABLE 可运行

        while (state!=Thread.State.TERMINATED){ // 只要线程不终止,就一直输出线程状态

            Thread.sleep(1500);
            state = thread.getState();
            // TIMED_WAITING 计时等待
            // TIMED_WAITING
            // TIMED_WAITING
            // ///
            // TERMINATED 被终止
            System.out.println(state);

        }

    }
}

线程优先级

  • java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。

  • 线程的优先级用数字表示,范围从1-10

    • 线程可以拥有的最小优先级。
      MIN_PRIORITY = 1;

    • 分配给线程的默认优先级。
      NORM_PRIORITY = 5;

    • 线程可以拥有的最大优先级。
      MAX_PRIORITY = 10

    优先级越高不一定先被执行,线程的执行是看cpu调度,优先级越高只是权重就越大了。例如100张彩票和1张彩票的中奖率。

  • 改变或获取优先级
    getPriority() setPriority(int newPriority)

package com.demo.threadDemo;

public class TestPriority {

    public static void main(String[] args) {
        // 主线程优先级
        System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());

        MyPriority myPriority = new MyPriority();

        Thread thread = new Thread(myPriority);
        Thread thread2 = new Thread(myPriority);
        Thread thread3 = new Thread(myPriority);
        Thread thread4 = new Thread(myPriority);

        thread2.setPriority(1);
        thread3.setPriority(4);
        thread4.setPriority(Thread.MAX_PRIORITY);

        thread4.start();
        thread.start();
        thread2.start();
        thread3.start();


        // 实测中,除了main线程,其他线程的顺序均可变化

        // main-->5
        //Thread-0-->5
        //Thread-1-->1
        //Thread-2-->4
        //Thread-3-->10





    }
}

class MyPriority implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
    }
}

守护(daemon)线程

  • 线程分为用户线程和守护线程

  • 虚拟机必须确保用户线程执行完毕
    如main函数

  • 虚拟机不用等待守护线程执行完毕
    如,后台记录操作日志,监控内存,垃圾回收等等

package com.demo.threadDemo;


// 测试守护线程
// 案例 上帝守护你
public class TestDaemon {

    public static void main(String[] args) {

        // 上帝就相当于一个守护线程
        // 你相当于用户线程

        God god = new God();

        You2 you2 = new You2();

        Thread thread = new Thread(god);
        // 默认false,表示用户线程,正常的线程都是用户线程
        thread.setDaemon(true);
        thread.start();


        new Thread(you2).start();



        // god 守护线程,单纯看我们的代码逻辑,是一个死循环,会无限执行下去
        // 但因为我们将它设置为守护线程,所以我们的用户线程 You2 执行完毕,它就自动结束了


    }




}




class God implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("上帝保佑着ni");
        }
    }
}





class You2 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 36500; i++) {
            System.out.println("你一生都开心的活着");
        }

        System.out.println("======goodbye!world!");
    }
}

java实现多线程的四种方式

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口
  4. 使用Executors框架创建线程池

因为两个接口实现的类似,所以也可以说实现多线程有三种方式,这个不用纠结。

继承Thread类

/**
1. 定义类,继承Thread类
2. 覆盖重写run方法(指定线程任务)
3. 创建Thread类的子类对象
4. 子类对象调用start方法,开启线程
**/
package com.demo.multithreadingDemo;

import java.util.stream.Stream;

public class MyThread extends Thread{
    @Override
    public void run() {
        Stream<Integer> iterate = Stream.iterate(0, (x) -> (x + 1));
        iterate.limit(5).forEach(System.out::println);
    }
}




package com.demo.multithreadingDemo;

import java.util.stream.Stream;

public class MultithreadingDemo {

    public static void main(String[] args) {


        MyThread myThread = new MyThread();

        myThread.start();

        Stream<Integer> iterate = Stream.iterate(100, (x) -> (x + 1));
        iterate.limit(5).forEach(System.out::println);



    }
}


/**
结果证明两个线程是交替执行的,且结果是不确定的
0
100
1
101
2
3
4
102
103
104
**/

实现Runnable接口

package com.demo.multithreadingDemo;


/**
 * 1.定义类实现Runnable接口
 * 2.重写run方法(指定线程任务)
 * 3.创建Runnable接口的实现类对象
 * 4.创建Thread类的对象,构造方法传递Runnable接口的实现类对象
 * 5.调用start方法开启线程
 */
public class MultithreadingDemo {

    public static void main(String[] args) {
        // main main()方法执行完成
        // Thread-0是lambda 实现的runnable线程

        Runnable runnable = () ->
                System.out.println(Thread.currentThread().getName() + "是lambda 实现的runnable线程");

        new Thread(runnable).start();

        System.out.println(Thread.currentThread().getName() + " main()方法执行完成");

    }
}

实现Callable接口

package com.demo.multithreadingDemo;

import java.util.concurrent.Callable;

public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {

        System.out.println(Thread.currentThread().getName() + " call()方法执行中...");

        return 0;
    }
}





package com.demo.multithreadingDemo;


import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 1.创建实现Callable接口的类myCallable
 * 2.以myCallable为参数创建FutureTask对象
 * 3.将FutureTask作为参数创建Thread对象
 * 4.调用线程对象的start()方法
 */
public class MultithreadingDemo {

    public static void main(String[] args) {

        // void run();
        // Runnable接口的run()没有返回值,没有抛出异常,子类覆盖重写,必须try catch

        // V call() throw Exception
        // Callable接口有返回值,可以指定泛型,抛出了异常,子类覆盖重写可以try catch,也可以throws

        FutureTask<Integer> task = new FutureTask<>(new MyCallable());
        new Thread(task).start();

        Integer result = null;
        try {
            result = task.get(); // 阻塞直到获取到结果
            System.out.println("返回的结果是:" + result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
        // Thread-0 call()方法执行中...
        //返回的结果是:0
        //main main()方法执行完成




    }
}

使用Executors框架创建线程池

Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。

主要有newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool,下面详细介绍这四种线程池

参数为Runnable接口的实现类

Future<?> future = executorService.submit(myThread);,返回的future对象是把线程任务的返回值封装成了对象,使用get()可以获取,而Runnable接口的run()方法的返回值为null,所以future封装的就是null

package com.demo.executorDemo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ExecutorDemo {

    public static void main(String[] args) {

        ExecutorService executorService = Executors.newFixedThreadPool(10);

        Runnable runnable = ()-> System.out.println(ExecutorDemo.class + Thread.currentThread().getName());

        Thread thread = new Thread(runnable);


        // class com.demo.executorDemo.ExecutorDemopool-1-thread-3
        //class com.demo.executorDemo.ExecutorDemopool-1-thread-2
        //class com.demo.executorDemo.ExecutorDemopool-1-thread-1
        Future<?> future = executorService.submit(thread);
        Future<?> future2 = executorService.submit(thread);
        Future<?> future3 = executorService.submit(thread);

        executorService.shutdown();

    }
}

四种方式的区别

  1. Thread和Runnable不能得到返回值也不能抛出异常,Callable可以获得返回值也能抛出异常。

  2. Thread、Runnable、Callable都不能控制资源,会导致资源耗尽。线程池可以控制资源,性能稳定。

  3. 继承Thread不灵活,因为Java只允许单继承(继承了Thread之后如果再想继承别的类就不可能了),实现Runnable接口避免单继承的局限性。

  4. 运行Callable任务可以拿到一个Future对象,表示异步计算的结果,等待计算的完成

    解释:实现Runnable接口,在main函数中执行线程,main线程和Runnable接口所代表的线程的两者执行顺序是不一定的;
    实现Callable接口,同样在main函数中执行,Callable接口代表的线程会一直运行到直至获取到call()中的返回值才会执行main线程。

  5. 通过Executor来启动线程比使用Thread的start方法更好,更易管理,效率更好。还能避免this逃逸问题(this逃逸的问题对于现在的我不太好理解,往后放

比如当前系统只有1G内存,顶多200个线程来执行,那么就直接给线程池控制好最大线程资源,即使就算有1000W个任务进来,我们也只能两百两百的慢慢执行,而不是1000W的任务瞬间全部启动。我们就达到了资源控制的效果,无论面对多高的并发,都不会让我们资源耗尽而导致系统崩溃 。

线程状态

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

sleep()和wait()的区别

方法所属类是否释放锁是否可以用在同步外面是否必须由锁对象调用是否需要捕获异常备注
static sleep()Thread不释放锁可以
wait()Object释放
notify()Object随机唤醒当前锁对象上多个等待线程其中的一个

线程同步

发生在,多个线程操作同一个资源。(并发)

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步。

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制 synchronized,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可。

例如我们说最常见的一种,就是我们A线程在进入方法后,拿到了count的值,刚把这个值读取出来还没有改变count的值的时候,结果线程B也进来的,那么导致线程A和线程B拿到的count值是一样的。

虽然锁机制解决了线程安全的问题,但存在以下问题,

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起
  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题

性能倒置:a线程执行完3ms且优先级高,b线程执行完3s且优先级低,此时如果先执行b线程,可以快速执行完毕的a线程却被阻塞,就引起了性能倒置。

导致线程安全问题的因素有哪些?

导致线程安全问题的主要问题有以下几点:

  1. 多线程同时执行并操作共享数据:多个线程同时执行是造成并发问题的根本原因,当多个线程访问和修改同一块数据时,可能导致数据覆盖,数据可见性等问题
  2. 非原子操作:某些操作看起来是单个语句,但是在计算机内部被分为多个步骤。如果这些操作在多线程环境下没有适当同步,就会导致线程安全问题
  3. 指令重排序:编译器和处理器为了优化性能,可能会对代码进行重新排序。这种重排序在单线程环境下是安全的,但是在多线程环境下可能就会出现意料之外结果
  4. 内存可见性:在多线程之下,一个线程对共享变量的修改不能立即被其他线程所发现。这是因为每一个线程都有自己的本地缓存,而且编译器和处理器可能会进行各种优化

解决线程安全问题的手段有哪些?

解决线程安全问题的手段主要有以下几种:

  • 使用锁:Java中提供了synchronized关键字和ReentrantLock类来实现锁机制。通过在可能出现线程安全问题的代码块或者方法上锁,可以确保只有一个线程可以执行这部分代码
  • 使用线程本地变量:ThreaadLocal类可以为每一个线程提供一个独立的变量副本。通过使用线程本地变量,可以让每个线程都有自己的变量,从而避免线程间的数据冲突
// 创建一个 ThreadLocal 对象
ThreadLocal<String> threadLocal = new ThreadLocal<>();
 
// 在每个线程中设置和获取线程本地变量的值
new Thread(() -> {
    threadLocal.set("线程1的值");
    System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
}).start();
 
new Thread(() -> {
    threadLocal.set("线程2的值");
    System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
}).start();
  • 使用线程安全的容器:如ConcurrentHashMap、CopyOnWriteArrayList

synchronized 同步方法

同步方法(底层锁是this,也就是对象本身),在方法上,返回值前加synchronized

synchronized 方法控制对 ”对象“ 的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。

缺陷:若将一个大的方法声明为synchronized 将会影响效率。

package com.demo.threadSyncDemo;

// 多个线程同时操作同一个对象
// 买火车票的例子
public class TestThread4 {


    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();


        new Thread(buyTicket, "周杰伦").start();
        new Thread(buyTicket, "周杰").start();
        new Thread(buyTicket, "周星星").start();
    }

}




class BuyTicket implements Runnable{


    // 票数
    private int ticketNums = 50;
    boolean flag = true;


    @Override
    public void run() {
        while (flag){
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }




    // synchronized 同步方法,锁的是this
    private synchronized void buy() throws InterruptedException {

        if (ticketNums<=0){
            flag = false;
            return;
        }

        Thread.sleep(100);

        System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums-- + "票");

    }
}



/**
 周杰拿到了第9票
 周杰伦拿到了第10票
 周星星拿到了第8票
 周星星拿到了第7票
 周杰伦拿到了第5票
 周杰拿到了第6票
 周杰伦拿到了第4票
 周杰拿到了第3票
 周星星拿到了第2票
 周杰伦拿到了第1票
 周杰拿到了第1票
 周星星拿到了第0票

周杰伦和周星星都拿到了第9张票,还有拿到第0张票的,这显然是不切合实际的,

发现并发问题,多个线程操作同一个资源的情况下,线程不安全,数据紊乱
**/



/*
加上同步关键字后


周杰伦拿到了第50票
周杰伦拿到了第49票
周杰伦拿到了第48票
周杰伦拿到了第47票
周杰伦拿到了第46票
周杰伦拿到了第45票
周杰伦拿到了第44票
周杰伦拿到了第43票
周杰伦拿到了第42票
周杰伦拿到了第41票
周杰伦拿到了第40票
周星星拿到了第39票
周星星拿到了第38票
周星星拿到了第37票
周星星拿到了第36票
周星星拿到了第35票
周星星拿到了第34票
周星星拿到了第33票
周星星拿到了第32票
周星星拿到了第31票
周星星拿到了第30票
周星星拿到了第29票
周星星拿到了第28票
周星星拿到了第27票
周星星拿到了第26票
周星星拿到了第25票
周星星拿到了第24票
周星星拿到了第23票
周星星拿到了第22票
周星星拿到了第21票
周星星拿到了第20票
周星星拿到了第19票
周星星拿到了第18票
周星星拿到了第17票
周星星拿到了第16票
周星星拿到了第15票
周星星拿到了第14票
周星星拿到了第13票
周星星拿到了第12票
周星星拿到了第11票
周星星拿到了第10票
周星星拿到了第9票
周星星拿到了第8票
周星星拿到了第7票
周星星拿到了第6票
周星星拿到了第5票
周星星拿到了第4票
周星星拿到了第3票
周星星拿到了第2票
周星星拿到了第1票

Process finished with exit code 0

 */

synchronized 同步代码块

同步代码块,synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

// 同步块
synchronized(Obj){}
  • Obj称之为同步监视器

    • Obj可以是任何对象,但是推荐使用共享资源(也就是要修改的对象或者变量)作为同步监视器
    • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身
  • 同步监视器的执行过程

    1. 第一个线程访问,锁定同步监视器,执行其中代码
    2. 第二个线程访问,发现同步监视器被锁定,无法访问
    3. 第一个线程访问完毕,解锁同步监视器
    4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
package com.demo.threadSyncDemo;

public class UnsafeBank {

    public static void main(String[] args) {

        Account account = new Account(100, "结婚基金");

        Drawing you = new Drawing(account, 50, "你");

        Drawing her = new Drawing(account, 100, "她");

        you.start();
        her.start();



        /*
         没加同步之前

         结婚基金余额为:-50
         结婚基金余额为:-50
         她手里的钱:100
         你手里的钱:50
         */




        /*
         结婚基金余额为:-50
         结婚基金余额为:-50
         你手里的钱:50
         她手里的钱:100

         在run方法上加synchronized,发现账户的扣减还是不对
         因为同步方法的锁是this,也就是Drawing,然而我们是对Account进行修改

         我们需要使用同步代码块,将Account锁起来
         锁的对象就是变化的量,也就是需要增删改的对象

         结婚基金余额为:50
         你手里的钱:50
         她钱不够,取不了
         */

    }

}



class Account{
    int money; // 余额
    String name; // 卡名

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }

}


// 银行,模拟取款
class Drawing extends Thread{

    Account account; // 账户
    // 取了多少钱
    int drawingMoney;
    // 现在手里有多少钱
    int nowMoney;


    public Drawing(Account account,int drawingMoney,String name){
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }


    // 取钱
    @Override
    public void run() {


        synchronized (account){
            // 判断有没有钱
            if (account.money-drawingMoney<0){
                System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
                return;
            }

            // sleep可以放大问题的发生性
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 卡内余额 = 余额 - 你取的钱
            account.money = account.money - drawingMoney;

            // 你手里的钱
            nowMoney = nowMoney + drawingMoney;


            System.out.println(account.name + "余额为:" +account.money);
            System.out.println(this.getName() + "手里的钱:" +nowMoney);
        }



    }
}

package com.demo.threadSyncDemo;

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

public class UnsafeList {

    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();

        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }

        // 担心main线程比上面循环的线程先跑完,
        Thread.sleep(3000);
        // 8980 该数不固定
        // 因为ArrayList不是线程安全的,所以有可能两个线程同时进入for循环,
        // 把两个元素添加到了同一个位置,后一个值就把前一个值给覆盖掉了
        // 导致list的大小小于10000
        System.out.println(list.size());
    }
}

同步锁

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

java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,知道线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。(目的:只有一个线程可执行)

  1. 锁对象 可以是任意类型。

  2. 多个线程对象 要使用同一把锁。
    注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着

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

Lock(锁)

  • 从jdk5.0开始,java提供了更强大的线程同步机制----通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当。

  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象

  • ReentrantLock(可重入锁)类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁,释放锁。

java.util.concurrent.locks.Lock接口 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

常用的实现类有ReentrantLock,ReentrantLock是可重入锁,非公平锁

// 获取锁的时候锁被占用就返回false,否则返回true
boolean tryLock()

// 获取锁
void lock()

// 释放锁
void unlock()
package com.demo.threadLock;

import java.util.concurrent.locks.ReentrantLock;

// 测试lock锁
public class TestLock {

    public static void main(String[] args) {
        TestLock2 testLock2 = new TestLock2();

        new Thread(testLock2).start();
        new Thread(testLock2).start();
        new Thread(testLock2).start();

    }


}



class TestLock2 implements Runnable{

    int ticketNums = 10;

    // 定义lock锁
    private final ReentrantLock lock = new ReentrantLock();

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

            try {
                lock.lock(); // 加锁
                if (ticketNums>0){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(ticketNums--);
                }else {
                    break;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock(); // 解锁
            }
        }
    }
}

synchronized和Lock的区别

  1. 实现层面不一样。synchronized 是 Java 关键字,JVM层面 实现加锁和释放锁;Lock 是一个接口,在代码层面实现加锁和释放锁

  2. 是否自动释放锁。synchronized 在线程代码执行完或出现异常时自动释放锁;Lock 不会自动释放锁,需要再 finally {} 代码块显式地中释放锁

  3. 获取锁成功是否可知。synchronized 无法得知是否获取锁成功;Lock 可以通过 tryLock 获得加锁是否成功

  4. 功能复杂性。synchronized 锁可重入、不可中断、非公平;Lock 可重入、可中断、可公平和不公平、细分读写锁提高效率

公平性:synchronized是非公平锁,而ReentrantLock可以选择时公平锁还是非公平锁,通过构造方法 new ReentrantLock() 时传入 boolean 值进行选择,为空默认 false 非公平锁,true 为公平锁(公平锁是一种锁的机制,它保证了多个线程按照申请锁的顺序去获得锁。也就是说,如果多个线程同时请求同一个锁,那么最先请求的线程将最先获得锁。这种机制确保了所有的线程都有机会获得锁,不会出现某个线程长时间等待锁而无法获得的情况)

性能:资源竞争不激烈,两者相差不多(即有大量线程同时竞争), 资源竞争激烈时,Lock的性能要远远优于synchronized

响应中断:synchronized是不可中断类型的锁,除非加锁的代码中出现了异常或者正常执行完成;而ReentrantLock则可以响应中断,可以通过 tryLock(long timeout, TimeUnit unit) 设置超时方法或者将 lockInterruptibly() 放到代码块中,调用 interrupt 方法进行中断

优先使用顺序:Lock > 同步代码块 > 同步方法

死锁

死锁,基本就是资源不够,互相需要对方资源却不肯放弃自身资源。N线程访问N资源,为了避免死锁,可以为其加锁并指定获取锁的顺序,这样线程按照顺序加锁访问资源,依次使用依次释放,可以避免死锁。

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止的情形。

某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生”死锁“的问题。

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示:
在这里插入图片描述
线程协作使用不当也会造成死锁。进程(这里只讨论线程吧,进程还不太懂)间彼此相互等待对方发来的消息,结果也会使得这些进程间无法继续向前推进。例如,进程A等待进程B发的消息,进程B又在等待进程A发的消息,可以看出进程A和B不是因为竞争同一资源,而是在等待对方的资源导致死锁。

死锁避免方法

产生死锁的四个必要条件:

  1. 互斥条件:一个资源被多个线程使用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

上面列出死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生

package com.demo.threadSyncDemo;

// 死锁:多个线程互相持有对方需要的资源,然后形成僵持
public class DeadLock {

    public static void main(String[] args) {
        Makeup g1 = new Makeup(0, "灰姑娘");
        Makeup g2 = new Makeup(1, "白雪公主");

        g1.start();
        g2.start();


    }
}



// 口红
class Lipstick{

}


// 镜子
class Mirror{

}


class Makeup extends Thread{

    // 需要的资源只有一份,用static来保证只有一份
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int choice; // 选择
    String name; // 使用化妆品的人

    public Makeup(int choice,String name){
        this.choice = choice;
        this.name = name;
    }

    @Override
    public void run() {
        // 化妆
        try {
            makeUp();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 化妆,互相持有对方的锁,就是需要拿到对方的资源
    private void makeUp() throws InterruptedException {


        // 灰姑娘获得口红的锁
        // 白雪公主获得镜子的锁
        // 程序执行到这里就卡住不动了
        // 这就是死锁,导致代码卡死
//        if (choice==0){
//            synchronized (lipstick){ // 获得口红的锁
//                System.out.println(this.name+"获得口红的锁");
//                Thread.sleep(1000);
//
//                synchronized (mirror) { // 一秒钟后想获得镜子
//                    System.out.println(this.name + "获得镜子的锁");
//                }
//
//            }
//        }else {
//            synchronized (mirror){
//                System.out.println(this.name+"获得镜子的锁");
//                Thread.sleep(2000);
//
//                synchronized (lipstick) {
//                    System.out.println(this.name + "获得口红的锁");
//                }
//
//            }
//        }


        //灰姑娘获得口红的锁
        //白雪公主获得镜子的锁
        //白雪公主获得口红的锁
        //灰姑娘获得镜子的锁
        // 
        if (choice==0){
            synchronized (lipstick){ // 获得口红的锁
                System.out.println(this.name+"获得口红的锁");
                Thread.sleep(1000);

            }

            synchronized (mirror) { // 一秒钟后想获得镜子
                System.out.println(this.name + "获得镜子的锁");
            }
        }else {
            synchronized (mirror){
                System.out.println(this.name+"获得镜子的锁");
                Thread.sleep(2000);

            }

            synchronized (lipstick) {
                System.out.println(this.name + "获得口红的锁");
            }
        }
    }
}

如何避免死锁(确保N个线程可以访问N个资源同时又不导致死锁)

  1. 指定加锁顺序。(网上来看这种最常用,不知道实际工作中是不是)
    所有线程按照相同的顺序获得资源的锁,不先获得顺序靠前的锁,无法获得后续的锁。则先获得锁的线程不会请求后获得锁的线程占用的资源,因为后获得锁的线程还没能获得先获得锁的线程未释放的锁,更无法占用先获得锁的线程还没获得的顺序靠后的锁。缺点:需要手动对锁的获得顺序进行分析。

使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了

  1. 指定加锁时限。
    线程指定超时时间,若无法获得锁的占用权,进行回退操作,并释放已占用的锁,经一段延时后再尝试进行任务。缺点 :线程过多的话,可能造成频繁回退,运行效率不高。
  2. 死锁检测。
    将线程和已获得锁的情况记录下来,定时检测是否所有死锁现象(线程循环等待现象),回退处于死锁状态的线程,延时后,重试这些线程,与添加加锁时限类似,缺点也同。

加锁顺序

当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。

如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。看下面这个例子:

Thread 1:
  lock A 
  lock B

Thread 2:
   wait for A
   lock C (when A locked)

Thread 3:
   wait for A
   wait for B
   wait for C

如果一个线程(比如线程3)需要一些锁,那么它必须按照确定的顺序获取锁。它只有获得了从顺序上排在前面的锁之后,才能获取后面的锁。

例如,线程2和线程3只有在获取了锁A之后才能尝试获取锁C(译者注:获取锁A是获取锁C的必要条件)。因为线程1已经拥有了锁A,所以线程2和3需要一直等到锁A被释放。然后在它们尝试对B或C加锁之前,必须成功地对A加了锁。

按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁(译者注:并对这些锁做适当的排序),但总有些时候是无法预知的。

加锁时限

(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)

另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(译者注:加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。

以下是一个例子,展示了两个线程以不同的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景:

Thread 1 locks A
Thread 2 locks B

// 尝试
Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked

Thread 1's lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.

Thread 2's lock attempt on A times out
Thread 2 backs up and releases B as well
// 随机
Thread 2 waits randomly (e.g. 43 millis) before retrying.

在上面的例子中,线程2比线程1早200毫秒进行重试加锁,因此它可以先成功地获取到两个锁。这时,线程1尝试获取锁A并且处于等待状态。当线程2结束时,线程1也可以顺利的获得这两个锁(除非线程2或者其它线程在线程1成功获得两个锁之前又获得其中的一些锁)。

需要注意的是,由于存在锁的超时,所以我们不能认为这种场景就一定是出现了死锁。也可能是因为获得了锁的线程(导致其它线程超时)需要很长的时间去完成它的任务。

此外,如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,但是如果是10个或20个线程情况就不同了。因为这些线程等待相等的重试时间的概率就高的多(或者非常接近以至于会出现问题)。
(译者注:超时和重试机制是为了避免在同一时间出现的竞争,但是当线程很多时,其中两个或多个线程的超时时间一样或者接近的可能性就会很大,因此就算出现竞争而导致超时后,由于超时时间一样,它们又会同时开始重试,导致新一轮的竞争,带来了新的问题。)

这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。写一个自定义锁类不复杂,但超出了本文的内容。后续的Java并发系列会涵盖自定义锁的内容。

死锁检测

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。

当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。

下面是一幅关于四个线程(A,B,C和D)之间锁占有和请求的关系图。像这样的数据结构就可以被用来检测死锁。
在这里插入图片描述

那么当检测出死锁时,这些线程该做些什么呢?

一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者注:原因同超时类似,不能从根本上减轻竞争)。

一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。

感觉上,保证加锁顺序是最好的,如果难以判断顺序的话,说明程序逻辑有问题。
延时退出的话,第一感不太好。有些锁是调用方加锁的,被调用的函数无法控制,如何释放所有锁就太麻烦了。
死锁检测的话,适合记录成日志,让程序员去调试bug。

加粗部分出自评论,仅参考,不一定正确

文章部分知识引自:
https://blog.csdn.net/ThinkWon/article/details/102021143
https://blog.csdn.net/gm371200587/article/details/88173030
https://www.cnblogs.com/luo841997665/p/4666324.html
https://blog.csdn.net/lasttemplar/article/details/109348336
https://www.cnblogs.com/it-deepinmind/p/13072269.html
https://www.bilibili.com/video/BV1V4411p7EF/?p=11&spm_id_from=pageDriver&vd_source=64c73c596c59837e620fed47fa27ada7
https://blog.csdn.net/qq_43842093/article/details/129104066
https://blog.csdn.net/weixin_33788465/article/details/114514024
https://blog.csdn.net/u013773608/article/details/130331937
https://blog.csdn.net/ls5718/article/details/51896159
https://www.nowcoder.com/questionTerminal/7192c9454277483d8711a7b4237a0bbe?orderByHotValue=1&questionTypes=000010&done=0&pos=27&mutiTagIds=639&onlyReference=false
https://blog.csdn.net/loss_rose777/article/details/135670893

一万年来谁著史,三千里外欲封侯。

李鸿章

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值