Java多线程基础

本文深入探讨了线程与并发的基础知识,包括多线程、多进程的概念,线程的创建与管理,线程状态与控制,以及线程间的同步与通信机制。详细讲解了线程安全问题的根源及解决方案,如死锁的预防,Lock锁的使用,生产者与消费者的模型,并介绍了线程池的原理与应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 前言

多进程也就是多任务,比如边吃饭边玩手机,看起来好像做了两件事,实际上大脑只在某一时刻做一件时间,只是太快了,导致看起来同时做两件事。

并发:一个CPU同时执行多个任务,多个事件可以在同一个时间间隔执行。

并行:多个CPU同时执行多个任务,多个事件可以在同一个时刻执行。

单核CPU和多核CPU的理解:

  • 单核CPU只有一个运算核心,是一种假的多线程,因为在一个时间单元内,只能执行一个任务,但是因为CPU的时间单元特别短,所以感觉不出来。
  • 多核CPU就有多个运算核心,是真的可以有多个任务同时执行。支持的线程数量更多,速度更快。其实也可以叫做拥有多个单核的CPU。

单线程和多线程的区别:

在这里插入图片描述

2. 程序,进程,线程

  • 程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念 。
  • 进程则是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位
  • 通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的的单位。

例如:在操作系统中运行的程序就是进程 :QQ等。一个进程可以有多个线程,如视频中同时听声音,看图像,看弹幕等等 。

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

线程注意事项

  • 线程就是独立的执行路径 。
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,Java中至少有3个线程:main函数就是一个线程,称为主线程;还有gc线程,管理内存的垃圾回收器;异常处理线程
  • 主线程是为系统的入口,用于执行整个程序。
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制。
  • 线程会带来额外的开销,如cpu调度时间,并发控制开销。
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。
  • 在一个进程中,如果开辟了多个线程,线程的运行由CPU使用调度算法安排调度,CPU是与操作系统紧密相关的,先后顺序是不能人为的干预的

线程的三大特性:(线程的安全的本质就是三大特性的问题)

  • 原子性:跟数据库事务的原子性一样,也就是要么一个线程的逻辑代码全部执行成功,要么全部执行失败。对于多线程也应如此,如果一个线程执行操作,其他线程不会干扰到它。
  • 可见性指的是一个线程修改了一个共享变量的值,其他线程可以立即读取到被修改的共享变量的值
  • 有序性:在多个线程执行时,会发现明明一些代码写在前面,一些代码写在后面,但是运行时,可能看到一些写在后面的代码先执行了。这是因为在程序运行时,编译器和处理器为了优化程序性能而对指令序列进行重排序,结果就造成了编写代码的顺序和执行代码时的顺序不一样。重排序可能会导致多线程程序出现内存可见性问题

参考:博客园:线程的三大特性

3. 创建线程

在Java中有三种创建线程的方式:

  • 继承Thread类:重写run()方法,实例化时调用start()方法执行。
  • 实现Runnable接口:重写run()方法,调用时需要借助Thread类,把该Runnable接口的子类传入到Thread类,然后调用start()方法执行。这里其实使用到了静态代理,可以点开Thread发现它也实现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;
}
/**
 * @author flunggg
 * @date 2020/7/22 21:30
 * @Email: chaste86@163.com
 */
// 步骤1:实现Callable接口,是泛型接口
public class CallableTest implements Callable<Boolean> {

    private String name;

    public CallableTest(String name) {
        this.name = name;
    }
	// 步骤2:重写call方法(跟重写run一样,只不过多了返回值,返回值跟接口泛型一样)
    @Override
    public Boolean call() throws Exception {
        for(int i = 1; i < 50; i++) {
            try {
                Thread.sleep(1);
                System.out.println(this.name +"在写代码!!!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

    public static void main(String[] args) {
        // 步骤3:创建实例
        CallableTest c1 = new CallableTest("小明");
        CallableTest c2 = new CallableTest("小红");
        // 步骤4:创建执行服务,并告诉要创建多少条线程
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        // 步骤5:提交执行
        Future<Boolean> f1 = executorService.submit(c1);
        Future<Boolean> f2 = executorService.submit(c2);
        // 可选步骤:可以获取结果
        Boolean aBoolean = null;
        try {
            aBoolean = f1.get();
            System.out.println(aBoolean);
            aBoolean = f2.get();
            System.out.println(aBoolean);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        // 步骤6:关闭服务
        executorService.shutdownNow();

    }
}

线程开启不一定立即执行,由CPU调度。

推荐使用第二种实现方式,因为Java具有单继承局限,而且使用Runnable接口创建的子类可以被多个线程共用(如果是打算需要共享的)。

第三种方式比较麻烦,但是可以有返回值,可以排除异常。但是面试可能会考。

4. 线程的状态

在这里插入图片描述

阻塞状态:

  • 线程调用sleep()方法或者join()方法会主动放弃所占用的资源,进入阻塞状态。(其他阻塞)
  • 线程调用一个阻塞式IO方法(比如读取一个文件),在方法返回前,该线程进入阻塞状态。(IO阻塞)
  • 线程试图获取一个同步锁(同步监视器),但该同步锁正被其他线程持有,该线程进入阻塞状态。(同步阻塞)
  • 线程中执行了wait(),会等待唤醒,也会进入阻塞状态。(等待阻塞)如果遇到有其他线程调用notify或者notifyAll就会唤醒变成就绪状态。
  • 程序调用了线程的suspend()方法将该线程挂起,进入阻塞状态。但是这个方法容易导致死锁。(等待阻塞)
    线程的状态在Thread类中使用枚举State存储:
    // 虽然有6个状态,但是通过JVM,最终的线程状态还是通过操作系统的线程状态实现
    public enum State {
        /**
         * 尚未启动的线程处于此状态
         */
        NEW,

        /**
		 * 在Java虚拟机中执行线程start处于此状态,Java把就绪状态和运行状态合二为一
         */
        RUNNABLE,

        /**
         * 被阻塞,等待获得锁
         */
        BLOCKED,

        /**
		 * 正在等着另一个线程执行特定动作的线程处于此状态
		 * 也就是等待事件发送
         */
        WAITING,

        /**
		 * 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
		 * 跟上一个一样,但是这里的等待有时间限制
         */
        TIMED_WAITING,

        /**
	 	 * 已经执行完的线程处于此状态
         */
        TERMINATED;
    }

示例:

public class StateTest {
    static class T implements Runnable {
        @Override
        public void run() {
            synchronized (this) {
                for(int i = 0; i < 3; i++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + " end!");
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t = new T();
        Thread t1 = new Thread(t, "A");
        Thread t2 = new Thread(t, "B");
        // NEW
        System.out.println(t1.getName() + "线程的状态:" + t1.getState());
        System.out.println(t2.getName() + "线程的状态:" + t2.getState());

        // start
        t1.start();
        t2.start();
        System.out.println(t1.getName() + "线程的状态:" + t1.getState());
        System.out.println(t2.getName() + "线程的状态:" + t2.getState());

        // 让线程进入阻塞状态
        Thread.sleep(1000);
        System.out.println(t1.getName() + "线程的状态:" + t1.getState());
        System.out.println(t2.getName() + "线程的状态:" + t2.getState());

    }

}

输出:

A线程的状态:NEW
B线程的状态:NEW
A线程的状态:RUNNABLE
B线程的状态:RUNNABLE
A线程的状态:TIMED_WAITING
B线程的状态:BLOCKED
A end!
B end!

5. 线程的停止

Thread类中提供了2个让线程停止的方法(stop(), destroy()),但是一看会发现被声明为@Deprecated,也就是官方建议我们别用。翻了下中文文档,使用它们会出现线程不安全的情况。

其实,最推荐的线程停止就是:让线程自然停止

5.1 stop() 的问题

stop() : 会破坏线程的原子性

/**
 * @author flunggg
 * @date 2020/7/30 15:16
 * @Email: chaste86@163.com
 */
public class DestoryThread implements Runnable{

    int a = 0;
    @Override
    public void run() {
        // 加了锁能够保证,a的结果每次都是0
        synchronized(this) {
            a++;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            a--;
            System.out.println(Thread.currentThread().getName() + "-->a:" + a);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        DestoryThread destoryThread = new DestoryThread();
        Thread t1 = new Thread(destoryThread,"1");
        Thread t2 = new Thread(destoryThread,"2");
        Thread t3 = new Thread(destoryThread,"3");
        t1.start();
        t2.start();
        t3.start();
        Thread.sleep(100);
        t1.stop();
    }

}

输出:

3-->a:1
2-->a:1

说明:

  • 启动了3个线程,因为加了同步锁,在一个线程执行时,其他的线程会被同步阻塞。
  • 现在线程t1先运行(如果t1不是最先运行的看不到效果,就重新run几次),其他线程会被同步阻塞,当t1执行到a++时,会睡眠1秒,此时a=1。(问题就出现在这一步)
  • 此时使用了stop方法,t1线程突然停止,所以t1线程并没有去执行a–,以及后面的代码,并会释放同步锁。
  • 线程t3进入运行状态,t2被同步阻塞,此时t3拿到的a的值为1,进行a++,后睡眠,因为加了锁不会被其他线程干扰,睡完后a–,输出:3–>a:1。
  • 线程t2进入运行状态,此时t2拿到的a的值为1,进行a++,后睡眠,睡完后a–,输出:2–>a:1。

5.2 destroy() 的问题

最初是打算用于破坏线程,但是不做任何清除,它所保持的任何监视器都会保持锁定状态(监视器是jvm的内容,这里就理解为锁)。

看了源码,发现它没有被实现,直接抛出NoSuchMethodError异常,而且源码上面有解释,说很容易出现死锁:如果一个线程在一个关键系统资源被破坏时持有一个保护它的锁,会一直持有它的锁,如果另一个线程打算访问此资源,将会一直处于阻塞状态,因为访问不了,导致死锁。

死锁:两个线程互相争夺同一资源,导致处于僵局,也就是谁都不退一步,都用不了该资源

5.3 使用标记来停止线程

/**
 * @author flunggg
 * @date 2020/7/30 16:31
 * @Email: chaste86@163.com
 */
public class StopTest implements Runnable{

    // 1. 线程中定义线程体使用的标识
    private boolean flag = true;

    @Override
    public void run() {
        // 2. 以该标识作为判断条件
        while(flag) {
            System.out.println("run......");
        }
    }

    // 3. 对外提供方法改变标识
    public void stop() {
        this.flag = false;
    }

    public static void main(String[] args) throws InterruptedException {
        StopTest stopTest = new StopTest();
        Thread thread = new Thread(stopTest);
        thread.start();
        
        // CPU太快了,我让它慢点
        Thread.sleep(1000);
        for(int i = 0; i < 1000; i++) {
            if(i == 99) {
                System.out.println("stop");
                stopTest.stop();
            }
        }
    }
}

6. 线程的其他方法

6.1 sleep()

Thread.sleep(),单位:毫秒:使得线程进入阻塞状态或者说休眠。

  • 该方法存在异常 InterruptedException;
  • 该方法时间达到后线程进入就绪状态;
  • 该方法可以模拟网络延时,倒计时等。
  • 每一个对象都有一个锁,sleep()不会释放锁; (这句话很重要,解决同步会用)

其实sleep()还可以放大一个操作存在的问题,像上面的例子,还有下面的资源共享问题。

6.2 yield()

Thraed.yield():礼让线程,让当前正在执行的线程暂停,但不阻塞将线程从运行状态转为就绪状态 ,如果下次要运行,还是看CPU如何调度(看CPU爸爸的心情)。

/**
 * @author flunggg
 * @date 2020/7/30 21:05
 * @Email: chaste86@163.com
 */
public class YieldTest implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程开始执行");
        // 无论加不加条件,礼让不一定成功
        if(Thread.currentThread().getName().equals("a")) {
            // 礼让
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + "线程结束执行");
    }

    public static void main(String[] args) {
        YieldTest yieldTest = new YieldTest();
        Thread t1 = new Thread(yieldTest, "a");
        Thread t2 = new Thread(yieldTest, "b");
        t1.start();
        t2.start();
    }
}

输出:

a线程开始执行
b线程开始执行
b线程结束执行
a线程结束执行

礼让不一定成功,比如:

在这里插入图片描述

按照逻辑,应该是a线程礼让后,b线程进来,然后b线程此时不执行礼让,直接结束,然后轮到a线程进入运行状态,输出结束。

而这里,a线程看起来不礼让,但是实际上a线程有礼让了,因为是看CPU爸爸的心情,此时CPU不让b线程开始执行,而是给a线程,所以a线程执行结束操作,随后轮到b线程。

6.3 join()

join():是一个对象方法,待此线程执行完成后,再执行其他线程,其他线程阻塞 。可以理解为插队。更准确的说:在A线程中调用B线程的join(),那么A线程会等待B线程执行完才开始执行A线程

join()有三个重载方法:

public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
public final synchronized void join(long millis, int nanos) throws InterruptedException

以上面的A和B线程来参考:

  • 无参的join调用的是join(long millis),传入的参数为0,表示A线程会一直等下去,直到B线程执行完。
  • 如果传入大于0的,表示A线程会等待一段时间,如果在该时间内B线程未执行完,则跳出,轮到A线程执行。
  • join(long millis)的底层调用wait()方法,而wait()方法会让出锁。对比与sleep(),它会一直保持锁
  • join()和sleep()可以被中断,中断抛出InterruptedException。

示例:

/**
 * @author flunggg
 * @date 2020/7/30 21:39
 * @Email: chaste86@163.com
 */
public class JoinTest implements Runnable {
    @Override
    public void run() {
        for(int i = 1; i < 100; i++) {
            System.out.println("vip子弟" + i + "来了,其他人给我让开");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        JoinTest joinTest = new JoinTest();
        Thread thread = new Thread(joinTest);
        thread.start();

        System.out.println("排队:");
        // 主线程
        for(int i = 1; i < 200; i++) {
            if(i == 100) {
                // 主线程调用thread线程的join
                // vip插队,在此期间只执行该线程
                thread.join();
            }
            System.out.println("普通子弟" + i + "来了");
        }
    }
}

注意:有时候可能电脑太好了,看不出效果,原本应该是并发执行的,可能先执行主线程后再执行副线程,可能先执行副线程再执行主线程,可能交替执行。

假设有多个线程,比如A线程,B线程,C线程,如果想要按照B,A,C的顺序执行,那么应该怎么做:

public class JoinTest {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for(int i = 1; i < 20; i++) {
                System.out.println(Thread.currentThread().getName() + "线程:" + i);
            }
        }, "B");
        
        Thread t2 = new Thread(()->{
            try {
                // 让A线程等待B线程执行完
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int i = 1; i < 20; i++) {
                System.out.println(Thread.currentThread().getName() + "线程:" + i);
            }
        }, "A");
        
        Thread t3 = new Thread(()->{
            try {
                // 让C线程等待A线程执行完
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int i = 1; i < 20; i++) {
                System.out.println(Thread.currentThread().getName() + "线程:" + i);
            }
        }, "C");

        // 以下的做法是错误的。
//        t1.start();
//        t1.join();
//        t2.start();
//        t2.join();
//        t3.start();
//        t3.join();

        // 正确的做法,在run方法中,调用要先执行的线程的join
        t1.start();
        t2.start();
        t3.start();
    }
}

6.4 线程名

对于实例化的Thread,可以使用getName()

也可以通过静态方法currentThread,获取当前线程对象,然后调用getName() 获取当前线程的名称,可以这样写:Thread.currentThread().getName()。

6.5 线程的优先级

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

线程的优先级用数字表示,API规定的范围从1~10,在Thread中也提供了3个常量,来表示优先级最小,中间和最大。使用数字也可以使用提供的常量也可以。

	public final static int MIN_PRIORITY = 1;

    public final static int NORM_PRIORITY = 5;

    public final static int MAX_PRIORITY = 10;

但是,优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看CPU爸爸的心情

Thread类提供了两个普通方法可以设置优先级和获取优先级:

    // 设置优先级
	public final void setPriority(int newPriority) {
        ThreadGroup g; // 这个我不懂,以后再回来看
        checkAccess(); // 检查是否安全 
        // 如果线程优先级超过最大优先级,或者小于最小优先级,抛出IllegalArgumentException异常
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        // 反正是把我们设置的优先级设置进去
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
    }
    
    // 获取优先级
    public final int getPriority() {
        return priority;
    }

示例:

/**
 * @author flunggg
 * @date 2020/7/31 1:07
 * @Email: chaste86@163.com
 */
public class PriorityTest implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程的优先级:" + Thread.currentThread().getPriority());
    }

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

        PriorityTest priorityTest = new PriorityTest();
        // 其他线程
        Thread t1 = new Thread(priorityTest, "A");
        Thread t2 = new Thread(priorityTest, "B");
        Thread t3 = new Thread(priorityTest, "C");
        Thread t4 = new Thread(priorityTest, "D");

        t1.setPriority(1);
        t1.start();

        t2.start();

        t3.setPriority(2);
        t3.start();

        t4.setPriority(10);
        t4.start();
    }
}

输出:

// 输出结果1:
main线程的优先级:5
D线程的优先级:10
B线程的优先级:5
A线程的优先级:1
C线程的优先级:2
// 输出结果2:    
main线程的优先级:5
D线程的优先级:10
B线程的优先级:5
C线程的优先级:2
A线程的优先级:1

由此可知

  • 主线程的优先级是5,线程默认的优先级也是5
  • 线程的优先级并不是说高就一定先执行,比如上面优先级1和2。但是越高的话先执行的可能性肯定越大,但还不是一定先执行。

6.6 守护线程

线程分为用户线程守护线程 ,虚拟机必须确保用户线程执行完毕 ,而不用等待守护线程执行完毕。

守护线程的应用:后台记录操作日志,监控内存,垃圾回收(java的gc垃圾回收器)。

如何设置守护线程,如下:

/**
 * @author flunggg
 * @date 2020/7/31 1:24
 * @Email: chaste86@163.com
 */
public class DaemonTest {
    public static void main(String[] args) {
        Thread god = new Thread(new God());
        god.setDaemon(true); // 默认为false,表示用户线程,也就是我们平常写的线程
        god.start();

        new Thread(new You()).start();
    }
}
class God implements Runnable {

    @Override
    public void run() {
        while(true) {
            System.out.println("老爷保佑!!");
        }
    }
}

class You implements Runnable {

    @Override
    public void run() {
        // 人生不过三万天
        for(int i = 1; i <= 36500; i++) {
            System.out.println("你每一天都开心的活着~~");
        }
        System.out.println("bye world");
    }
}

7. 线程同步

7.1 引入

来看看一个抢票的例子:

public class TicketTest {

    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(ticket, "小明").start();
        new Thread(ticket, "小红").start();
        new Thread(ticket,  "黄牛").start();
    }
}

class Ticket implements Runnable {


    private int ticket = 10;
    private boolean flag = true;
    @Override
    public void run() {
        while(flag) {
            buy();
        }
    }

    public  void buy() {
        if(ticket < 1) {
            flag = false;
            return ;
        }
        try {
            // 延迟一下,不然看不到效果
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // Thread.currentThread().getName() 获取当前线程的名称,可以在构造函数那里定义
        System.out.println(Thread.currentThread().getName() + " 买了第" + ticket-- + "票");
    }
}

输出的答案很奇怪:

小明 买了第10票
小红 买了第9票
黄牛 买了第8票
小红 买了第7票
小明 买了第6票
黄牛 买了第5票
黄牛 买了第4票
小明 买了第3票
小红 买了第2票
黄牛 买了第1票
小红 买了第0票
小明 买了第-1票

解析:只说为什么有0和-1,结合上面注释的三段代码。

  • 每一个线程都有自己的工作区,当ticket=1时,A线程都可以进入第二段代码,此时A线程会休眠。
  • B线程在A线程还没醒前,看到了ticket的值是1,此时B线程也进来到第二段代码,B线程休眠。(关键点!!)
  • 此时假设A线程和B线程还没醒,那么C线程看到ticket还是为1,所以进入第二段代码,C线程休眠。
  • 结果,A线程醒后,执行第三段代码,ticket的值为0;然后B线程醒后,也执行第三段代码,此时ticket的值为-1;C线程醒后,执行第三段代码,此时ticket的值为-2。
  • 然后下一轮,各个线程判断到ticket<1,结束。
  • (上面的结果是A线程醒了对ticket–,导致C线程判断到ticket<1,所以就没有ticket=-2)

原因:这是因为多个线程操作同一个资源,互相争夺修改数据,导致数据混乱,这可以叫做线程不安全,也可以叫线程不同步的表现。

7.3 同步概念

线程同步:处理多线程问题时 , 多个线程访问同一个对象 , 并且某些线程还想修改这个对象 .这时候我们就需要线程同步。线程同步其实就是一种等待机制 , 多个需要同时访问此对象的线程进入这个对象的等待池形成队列, 等待前面线程使用完毕 , 下一个线程再使用 。

临界资源:指的是一些虽作为共享资源却又无法同时被多个线程共同访问的共享资源。当有进程在使用临界资源时,其他进程必须依据操作系统的同步机制等待占用进程释放该共享资源才可重新竞争使 用共享资源。像上面的仓库就是一个临界资源。

同步虽然保证安全性,但是会影响性能

还有并不是所有的事情都用同步的方式去解决,比如我们多个人同时访问同一个网页就是异步(不同步)的表现,虽然共享资源,但是我们只是读取,而不修改,所以不会导致数据混乱,而且性能好。

异步:就是不同步,不安全,性能好

7.3 使用同步

为了保证数据在多线程中的正确性 , 在访问时加入锁机制synchronized , 当一个线程获得对象的排它锁(如果是新手,先不管什么锁,反正就当成锁就行) , 独占资源 , 其他线程必须等待,使用后释放锁即可。加锁存在以下问题 :

  • 一个线程访问共享资源持有锁,如果其他线程访问时会被挂起,进行等待释放锁 ;
  • 在多线程竞争下 , 加锁 , 释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题 ;
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置 , 引起性能问题 。

synchronized是一个java中的关键字,可以修饰方法和代码块。

如果修饰方法:

  • 该方法称为同步方法。

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

  • 缺陷 : 若将一个逻辑代码很多的方法申明为synchronized 将会影响效率 。

  • synchronized 默认锁的是 对象(this)。

  • 执行过程:

    1. 第一个线程访问该同步方法,该线程获取到锁,开始执行方法中的代码。
    2. 第二个线程访问该同步方法,发现被加锁,等待。
    3. 第一个线程执行完返回,释放锁。
    4. 第二个线程访问,发现没有被加锁,第二个线程锁定然后执行里面的代码。

如果修饰代码块:

  • 该代码块称为同步块,同步块 : synchronized (Obj ) { }
  • Obj 称之为 同步监视器,Obj 可以是任何对象 , 但是推荐使用共享资源作为同步监视器,同步方法中无需指定同步监视器 , 因为同步方法的同步监视器就是this , 就是这个对象本身 , 或者是 class (类)
  • 同步监视器的执行过程
    1. 第一个线程访问 , 锁定同步监视器 , 执行其中代码。
    2. 第二个线程访问 , 发现同步监视器被锁定 , 无法访问。
    3. 第一个线程访问完毕 , 解锁同步监视器。
    4. 第二个线程访问, 发现同步监视器没有锁 , 然后锁定并访问 。

共享资源在哪个对象手里,就用哪个对象把代码块锁起来

实现同步不止用synchronized ,还有其他的,待说。

7.4 改正例子

抢票例子:

public class TicketTest {

    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(ticket, "小明").start();
        new Thread(ticket, "小红").start();
        new Thread(ticket,  "黄牛").start();
    }
}

class Ticket implements Runnable {


    private int ticket = 10;
    private boolean flag = true;
    @Override
    public void run() {
        while(flag) {
            buy();
        }
    }

    // synchronized 同步方法,锁的是this(当前的Ticket)
    // 这是实例化方法,所以锁this生效
    public synchronized void buy() {
        if(ticket < 1) {
            flag = false;
            return ;
        }
        try {
            // 延迟一下,不然看不到效果
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // Thread.currentThread().getName() 获取当前线程的名称,可以在构造函数那里定义
        System.out.println(Thread.currentThread().getName() + " 买了第" + ticket-- + "票");
    }
}

输出:

小明 买了第10票
黄牛 买了第9票
小红 买了第8票
黄牛 买了第7票
小明 买了第6票
黄牛 买了第5票
黄牛 买了第4票
黄牛 买了第3票
黄牛 买了第2票
小红 买了第1票

网上的大部分抢票例子的执行结果就只输出一个线程,看不到抢。因为他们就把synchronized放在run方法,导致一个线程可以访问进来时,都抢完了票才让别的线程进入run,太不公平了。

所以得看好synchronized放在哪。我这里是放在另一个方法buy(),也就是所有的线程都可以跑进while,在while中争着执行buy(),每执行一次bug()后,所有线程就都会抢一次。

银行取钱例子:(跟上面的例子的修改方式不同)

public class Bank {

    public static void main(String[] args) {
        Account account = new Account("农行", 100);
        
        // 注意跟上一个例子的区别,虽然account对象是同一个,但是Drawing有两个对象
        Drawing xiaoming = new Drawing(account, 50, "小明");
        Drawing xiaohong = new Drawing(account, 70, "小红");
        
        new Thread(xiaoming).start();
        new Thread(xiaohong).start();
    }


}
// 账户
class Account {
    String name;
    int money;

    public Account(String name, int money) {
        this.name = name;
        this.money = money;
    }
}
// 取钱
class Drawing implements Runnable {
    Account account;
    int drawingMoney;
    int nowMoney;
    String name;
    public Drawing(Account account, int drawingMoney, String name) {
        this.account = account;
        this.drawingMoney = drawingMoney;
        this.name = name;
    }

    @Override
    public void run() {
        // 看看有没有钱
        if(account.money - drawingMoney < 0) {
            System.out.println(name + ",您好,钱不足以取出");
            return ;
        }

        // 让问题出现
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 卡内余额
        account.money = account.money - drawingMoney;
        // 手里的钱
        nowMoney += drawingMoney;

        System.out.println(account.name + "的钱还剩余" + account.money);
        System.out.println(name + "拿取了" + drawingMoney);
    }
}

输出:

农行的钱还剩余50
小明拿取了50
农行的钱还剩余-20
小红拿取了70

修改:

如果使用synchronized修饰run方法,会发现结果还是错误。上面说了synchronized修饰方法默认是锁this,也就是锁住当前对象,而这里创建了两个对象(小明和小红),然后再把这两个对象加入到两个线程(互不干扰)。调用同步run时,各自都可以进去跑run。要跟抢票例子区分好,抢票的例子是多个线程去争夺一个资源,这里是2个线程各自使用自己的资源(虽然Account是同一个)。

所以得锁住类,就是一个class(反射的内容),只需要知道一个类无论有多少个实例但只有一个class,所以锁住class就能保证同步,比如:

    @Override
    public void run() {
        synchronized(Drawing.class) {
			// 其他不改
        }
    }

但是,其根本原因就是操作了相同的Account对象,也就是共享资源。所以还可以这样:

    @Override
    public void run() {
        synchronized(account) { // 锁住Account的对象
			// 其他不改
        }
    }

还可以这样:

    @Override
    public void run() {
        synchronized(Account.class) { // 锁住Account的class
			// 其他不改
        }
    }

需要知道以下三点,就不会搞错,转自:狂神多线程的评论区,用户名:NULL错误

  • 对于普通同步方法,锁是当前实例对象。 如果有多个实例,那么锁对象必然不同无法实现同步。
  • 对于静态同步方法(synchronized修饰的方法前面加static就变成锁class),锁是当前类的class。如果有多个实例,但是锁的是用一个class,可以完成同步。
  • 对于同步方法块,锁是synchronized括号里配置的对象。可以锁this,class。看看共享资源是哪一个。

8 死锁

死锁:就是多个线程各自占用一些资源,并互相等待其他线程占用的资源释放才可以运行。

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

public class DeadLock {
    public static void main(String[] args) {
        Makeup makeup1 = new Makeup("小明", 0);
        Makeup makeup2 = new Makeup("小红", 1);
        new Thread(makeup1).start();
        new Thread(makeup2).start();
    }
}

// 口红
class Lipstick {

}
// 镜子
class Mirror {

}
class Makeup implements Runnable {

    // 使用静态保证只有一个镜子和口红
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    private String name;
    private int choose;

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

    @Override
    public void run() {
        use();
    }

    public void use() {
        if(choose == 0) {
            synchronized (lipstick) {
                System.out.println(name + "拿到口红");
                System.out.println("休息一下,等待拿镜子");
                // 睡一睡
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (mirror) {
                    System.out.println(name + "放开口红");
                    System.out.println(name + "拿到镜子");
                }
            }

        } else {
            synchronized (mirror) {
                System.out.println(name + "拿到镜子");
                System.out.println("休息一下,等待拿口红");
                // 睡一睡
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lipstick) {
                    System.out.println(name + "放开镜子");
                    System.out.println(name + "拿到口号");
                }
            }

        }
    }
}

// 怎么改,不要两个锁嵌套再一起。

产生死锁的根源就是:

  • 竞争的资源不够。
  • 进程的调度顺序不当。拿到了一个锁,走完就赶紧释放。

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

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

除了第一个条件,其他的条件破坏一个就可以解除死锁。

这都是操作系统的内容。

9. Lock锁

从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。

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

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

使用:

public class TicketTest {

    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(ticket, "小明").start();
        new Thread(ticket, "小红").start();
        new Thread(ticket,  "黄牛").start();
    }
}

class Ticket implements Runnable {

    Lock lock = new ReentrantLock();
    private int ticket = 10;
    private boolean flag = true;
    @Override
    public void run() {
        while(flag) {
            buy();
        }
    }

    // synchronized 同步方法,锁的是this
    public void buy() {
        try {
            lock.lock();
            if(ticket < 1) {
                flag = false;
                return ;
            }
            try {
                // 延迟一下,不然看不到效果
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // Thread.currentThread().getName() 获取当前线程的名称,可以在构造函数那里定义
            System.out.println(Thread.currentThread().getName() + " 买了第" + ticket-- + "票");
        } finally {
            // 需要手动释放锁
            lock.unlock();
        }

    }
}

synchronized 与 Lock 的区别:

  • Lock是显式锁(手动开启和关闭锁),synchronized是隐式锁,出了作用域自动释放 。
  • Lock只有代码块锁,synchronized有代码块锁和方法锁 。
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

10. 生产者与消费者

引入生产者-消费者问题:有一群生产者进程在生产产品,并将这些产品提供给消费者进程进行消费,生产者进程和消费者进程可以并发执行,在两者之间设置了一个具有n可缓冲区的缓冲池,生产者进程需要将所生产的产品放到一个缓冲区中,消费者进程可以从缓冲区取走产品消费。

遇到的问题

  • 既然是并发,生产者和消费者使用一个缓冲区,那么得保证生产者的生产是一个完整的过程,消费者在消费时也是一个完整的过程。

  • 可能会出现生产者生产速度和消费者消费速度不一致问题,比如生产者在缓冲区已经生产满了,如果接下来几个还是生产者,打算生产发现缓冲区满了,那么就不执行生产,丢失了这次生产;或者消费者打算消费时,发现缓冲区为空,如果接下来几个都是消费者,那么他们都不消费,直接“走人”。

解决

  • 对于第一个问题,只需要使用同步方式解决就行。
  • 对于第二个问题,涉及到生产者和消费者之间的通信问题,比如;
    • 当生产者发现缓冲区满了后,自己等着它消费再继续生产。每次生产完,就通知消费者消费(下一次如果还是生产者也行,反正缓冲区满就得等待)。
    • 当消费者发现缓冲区为空时,自己等着它生产完再消费,别“走人”。每次消费完,就去通知生产者生产(下一次如果还是消费者也行,反正缓冲区空就得等待)。

所以单单用synchronized是不够的,虽然可以阻止并发更新同一个共享资源,实现同步,但是synchronized不能用来实现不同线程之间的信息传递(通信)。

java中提供了几个方法来解决线程的通信问题:

方法名作用
wait()表示线程一直等待,知道其他线程通知,与sleep不同,会释放锁。
wait(long timeout)指定等待的毫秒数
notify()唤醒一个处于等待状态的线程
notifyAll()唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调用

这些都是是Object类的方法 , 都只能在同步方法或者同步代码块中使用,否则会抛出异常IllegalMonitorStateException

生产者-消费者模型有几种生产方式,先搞两种:

实现方式一,信号灯法或者红绿灯法:

/*
 * 假设缓冲区为1
 * 当生产者生产1个时,就停止生产,并且通知消费者消费
 * 当消费者消费1个时,就停止消费,并且通知生产者生产
*/
class Store {
    private int num = 0;
    private boolean flag = false; 
    public synchronized void produce() {
        // 如果可以消费的话
        if(flag) {
            try {
                System.out.println("仓库已满,待消费");
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 如果需要生产
        num++;
        System.out.println("生产后,产品数为:" + this.num);
        flag = true;
        // 通知消费
        this.notifyAll();
    }

    public synchronized void consume() {
        // 如果可以生产的话
        if(!flag) {
            try {
                System.out.println("仓库为空,待生产");
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 如果需要消费
        num--;
        System.out.println("消费后,产品数为:" + this.num);
        flag = false;
        // 通知消费
        this.notifyAll();
    }

    public int getNum() {
        return num;
    }
}

/*
 * 消费者
 */
class Consumer implements Runnable {

    private Store store;

    Consumer(Store store) {

        this.store = store;
    }

    @Override
    public void run() {
        // 生产20次
        for (int i = 0; i < 20; i++) {
            store.consume();

        }
    }
}

/*
 * 生产者
 */
class Producer implements Runnable {

    private Store store;

    Producer(Store store) {
        this.store = store;
    }

    @Override
    public void run() {
        // 消费20次
        for (int i = 0; i < 20; i++) {
            store.produce();

        }
    }
}

public class Main {

    public static void main(String[] args) {
        Store store = new Store();
        Producer p1 = new Producer(store);
        Consumer c1 = new Consumer(store);
        Thread t1 = new Thread(p1);
        Thread t2 = new Thread(c1);
        t1.start();
        t2.start();

        // join 等待线程执行完才执行下面
        try {
            t1.join();
            t2.join();
            System.out.println("最终仓库剩余数:" + store.getNum());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

实现二:

/*
 * 刚开始仓库数为0,仓库最大存储为10
 * 现在要生产20次,消费20次,那么理想中最终仓库数应该为0
 * 使用多线程来并发模仿2个生产,2个消费
 */
class Store {
    private int num = 0;

    public synchronized void produce() {
        // 首先,如果仓库满的时候就得等待生产
        while (num >= 10) {
            // 为什么要循环?
            // 因为如果有多个生产者,当仓库满的时候,假设x1,x2,x3三个生产者在等待,
            //      当消费者消费一个后,会唤醒其他所有线程,
            //      假设此时唤醒了x1,生产了,此时num=10,然后会唤醒其他所有线程,
            //      假设此时x2也被唤醒了,那么x2会执行下面的代码,也就是num++,num变成了11,超过了缓冲区的大小
            // 在生产者线程被唤醒时,重新判断缓冲区
            // 如果只是一个生产者和一个消费者就if就行
            // 等待消费
            try {
                System.out.println("仓库已满,待消费");
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.num = this.num + 1;
        System.out.println("生产后,产品数为:" + this.num);
        // 每次生产完休息
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 休息通知消费
        this.notifyAll();

    }

    public synchronized void consume() {

        // 等待生产
        // while的原因同上
        while (num <= 0) {
            try {
                System.out.println("仓库已空,待生产");
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.num = this.num - 1;
        System.out.println("消费后,产品剩余数为:" + this.num);
        // 等待消费
        this.notifyAll();


    }

    public int getNum() {
        return num;
    }
}

/*
 * 消费者
 */
class Comsumer implements Runnable {

    private Store store;

    Comsumer(Store store) {

        this.store = store;
    }

    @Override
    public void run() {
        // 生产20次
        for (int i = 0; i < 20; i++) {
            store.consume();

        }
    }
}

/*
 * 生产者
 */
class Producer implements Runnable {

    private Store store;

    Producer(Store store) {
        this.store = store;
    }

    @Override
    public void run() {
        // 消费20次
        for (int i = 0; i < 20; i++) {
            store.produce();

        }
    }
}

public class Main {

    public static void main(String[] args) {
        Store store = new Store();
        Producer p1 = new Producer(store);
        Producer p2 = new Producer(store);
        Comsumer c1 = new Comsumer(store);
        Comsumer c2 = new Comsumer(store);
        Thread t1 = new Thread(p1);
        Thread t2 = new Thread(p2);
        Thread t3 = new Thread(c1);
        Thread t4 = new Thread(c2);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        // join 等待线程执行完才执行下面
        try {
            t1.join();
            t2.join();
            t3.join();
            t4.join();
            System.out.println("最终仓库剩余数:" + store.getNum());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

11 线程池

线程池是JUC内容,先入个门

如果经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

所以提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用

优点

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理(…)
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止

JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors

  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
    • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
    • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行Callable
    • void shutdown() :关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

不止这可以创建线程池还有其他

public class PoolTest {
    public static void main(String[] args) {
        // 创建大小为5的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        executorService.submit(new Pool());
        executorService.submit(new Pool());
        executorService.submit(new Pool());
        executorService.submit(new Pool());
        executorService.submit(new Pool());
        executorService.shutdown();
    }
}
class Pool implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值