多线程与并发

1 基础概念

1.1 程序,进程,线程

  • 程序:是为了完成某个特定的任务,而用某种语言编写的一组指令的集合,即指的是一段静态的代码,静态对象。

  • 进程:是程序的一次执行过程,或是一个正在运行的程序。是一个动态的过程:有它自身的产生、存在、和消亡的过程——生命周期。

    • 程序是静态的,进程是动态的。系统在运行时会为每个进程分配不同的内存空间。
  • 线程:进程可进一步细化为线程,是一个程序内部的执行的路径。若一个进程同一时间并行执行多个线程,就是支持多线程的。线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(PC),一个进程中的多个线程共享相同的内存单元/内存地址空间,可以访问相同的变量和对象,这就使得线程间的通信更简便、高效。但多个线程操作共享系统资源可能会带来安全隐患。

    • java程启动后,main线程,gc线程,异常处理线程

1.2 并行与并发

  • 并行:同时执行多个任务或操作。在计算机中,多核处理器可以同时执行多个指令,每个核心可以独立地执行不同的任务。这使得计算速度更快,可以更高效地处理多个任务。

  • 并发:在一段时间内同时处理多个任务或操作,虽然同一时刻只能执行一个任务,但通过快速地切换执行上下文,给人一种同时执行的感觉。常见的例子是操作系统通过时间片轮转调度算法,在多个进程之间进行切换,使得它们看起来是同时运行的。

  • 并行是真正的同时执行多个任务,而并发是在有限的时间内交替地执行多个任务。

  • 在某些情况下,并行和并发可以同时存在,比如在一个多核处理器上运行多个进程,每个进程内部又有多个线程并发执行。但在其他情况下,并行和并发可能是相互独立的,如在单核处理器上的多任务处理。

完成一个任务需要多少个线程

  • io密集型 cpu核数*2
  • cpu密集型 cpu核数

1.3 线程使用场景

线程是计算机中实现并发的一种方式,可以在同一个进程内同时执行多个任务。以下是一些常见的线程使用场景:

  • 多任务处理:线程适用于处理多个相对独立的任务,例如同时下载多个文件、处理多个客户端请求等。通过将不同任务分配给不同的线程,可以提高系统的响应速度和并发处理能力。

  • 用户界面响应:在图形用户界面(GUI)应用程序中,使用线程可以确保界面的响应性能。主线程负责处理用户界面的交互和响应,而其他线程负责后台任务,如数据加载、计算等。这样可以避免用户界面的阻塞,提供流畅的用户体验。

  • 并发服务器:在服务器应用程序中,使用线程可以同时处理多个客户端请求。每个客户端请求可以在一个独立的线程中进行处理,从而实现并发处理,提高服务器的吞吐量和响应能力。

  • 异步编程:线程还可以用于实现异步编程模型。通过在后台启动线程执行耗时的操作,可以防止主线程被阻塞,使得程序能够继续进行其他任务。异步编程常用于网络通信、文件读写等耗时操作。

以下是一些具体的线程使用场景实例:

  • 图片处理应用:假设有一个图片处理应用程序,用户可以选择上传多张图片进行处理。在这种情况下,可以使用线程池来管理图片处理任务。每个上传的图片可以在一个独立的线程中进行处理,从而实现并发处理,提高处理速度,并且不会阻塞用户界面。

  • 多线程下载器:考虑一个下载器应用程序,用户可以同时下载多个文件。通过使用多线程技术,可以将每个文件的下载任务分配到不同的线程中执行。这样可以实现同时下载多个文件,并加快下载速度。

  • 聊天程序:在实时聊天程序中,可以使用线程来处理接收和发送消息的任务。接收消息可以在后台线程中持续监听消息到达,而发送消息也可以在另一个线程中进行。这样可以保持用户界面的响应性,同时处理消息的接收和发送。

  • 并发服务器:对于一个并发服务器,可以使用线程池来处理客户端请求。每个客户端连接可以在一个独立的线程中进行处理,从而实现并发处理能力。这样可以确保服务器能够同时处理多个客户端请求,提高系统的性能和响应能力。

2 线程原理

2.1 线程的调度与时间片

由于 CPU 的计算频率非常高,每秒计算数十亿次,于是,可以将 CPU 的时间从毫秒的维度进行分段,每一小段叫做一个 CPU 时间片。不同的操作系统、不同的处理器,线程的 CPU 时间片长度都不同。假定操作系统的线程一个时间片的时间长度为 20 毫秒(比如 Windows XP),在一个 2GHz 的 CPU 上,那么一个时间片可以进行计算的次数是: 20 亿/(1000/20) =4 千万次,也就是说,一个时间片内的计算量是非常巨大的。 目前操作系统中主流的线程调度方式大都是:基于 CPU 时间片方式进行线程调度。线程只有得到 CPU 时间片,才能执行指令,处于执行状态;没有得到时间片的线程,处于就绪状态,等待系统分配下一个 CPU 时间片。由于时间片非常短,在各个线程之间快速地切换,表现出来特征是很多个线程在“同时执行”或者“并发执行”。线程的调度模型,目前主要分为两种调度模型:分时调度模型、抢占式调度模型。
(1)分时调度模型——系统平均分配 CPU 的时间片,所有线程轮流占用 CPU。分时调度模型在时间片调度的分配上,所有线程人人平等。
下图就是一个分时调度的简单例子:三个线程,轮流得到 CPU 时间片;一个线程执行时,另外两个线程处于就绪状态
(2)抢占式调度模型——系统按照线程优先级分配 CPU 时间片。优先级高的线程,优先分配 CPU 时间片;如果所有的就绪线程的优先级相同,那么会随机选择一个;优先级高的线程获取的 CPU 时间片相对多一些。 由于目前大部分操作系统都是使用抢占式调度模型进行线程调度。 Java 的线程管理和调度是委托给了操作系统完成的,与之相对应, Java 的线程调度也是使用抢占式调度模型。

2.2 线程状态

在不同的系统中线程状态的分类不同

操作系统,线程的状态

java的线程状态

得到java的线程状态

public Thread.State getState(); //返回当前线程的执行状态,一个枚举类型值

Thread.State 是一个内部枚举类,定义了 6 个枚举常量,分别代表 Java 线程的 6 种状态,具体如下:

public static enum State {
     NEW, //新建
     RUNNABLE, //可执行:包含操作系统的就绪、运行两种状态
     BLOCKED, //阻塞  -> 操作系统线程中的阻塞
     WAITING, //等待  -> 操作系统线程中的阻塞
     TIMED_WAITING, //计时等待 -> 操作系统线程中的阻塞
     TERMINATED; //终止
 }

1. NEW 状态
通过 new Thread(…)已经创建线程,但尚未调用 start()启动线程,该线程处于 NEW(新建)状态。虽然前面介绍了4种方式创建线程,但是其中的其他三种方式,本质上都是通过new Thread( )创建的线程,仅仅是创建了不同的 target 执行目标实例(如 Runnable 实例)。
2. RUNNABLE 状态
Java 把就绪(Ready)和执行(Running)两种状态合并为一种状态:可执行(RUNNABLE)状态(或者可运行状态)。调用了线程的 start()实例方法后,线程就处于就绪状态;此线程获取到 CPU 时间片后,开始执行 run( )方法中的业务代码,线程处于执行状态。

(1)就绪状态就绪状态仅仅表示线程具备运行资格,如果没有被操作系统的调度程序挑选中,线程就永远是就绪状态;当前线程进入就绪状态的条件,大致包括以下几种:

  • 调用线程的 start()方法,此线程进入就绪状态。
  • 当前线程的执行时间片用完。
  • 线程睡眠(sleep)操作结束。
  • 对其他线程合入(join)操作结束。
  • 等待用户输入结束。
  • 线程争抢到对象锁(Object Monitor)。
  • 当前线程调用了 yield 方法出让 CPU 执行权限。
    (2)执行状态
    线程调度程序从就绪状态的线程中选择一个线程,作为当前线程时线程所处的状态。这也是线程进入执行状态的唯一方式。
    3. BLOCKED 状态
    处于阻塞(BLOCKED)状态的线程并不会占用 CPU 资源,以下情况会让线程进入阻塞状态:
    (1)线程等待获取锁 等待获取一个锁,而该锁被其他线程持有,则该线程进入阻塞状态。当其他线程释放了该锁,并且线程调度器允许该线程持有该锁时,该线程退出阻塞状态。
    (2) IO 阻塞 线程发起了一个阻塞式 IO 操作后,如果不具备 IO 操作的条件,线程会进入阻塞状态。 IO 包括磁盘 IO、 网络 IO 等。 IO 阻塞的一个简单例子:线程等待用户输入内容后继续执行。
    4. WAITING 状态
    处于 WAITING(无限期等待)状态的线程不会被分配 CPU 时间片,需要被其他线程显式地唤醒,才会进入就绪状态。线程调用以下 3 种方法,会让自己进入无限等待状态:
    ● Object.wait() 方法,对应的唤醒方式为: Object.notify() / Object.notifyAll()。
    ● Thread.join() 方法,对应的唤醒方式为:被合入的线程执行完毕。
    ● LockSupport.park() 方法,对应的唤醒方式为: LockSupport.unpark(Thread)。
    5. TIMED_WAITING 状态
    处于 TIMED_WAITING(限时等待)状态的线程不会被分配 CPU 时间片,如果指定时间之内没有被唤醒,限时等待的线程会被系统自动唤醒,进入就绪状态。以下 3 个方法会让线程进入限时等待状态:
    ● Thread.sleep(time) 方法,对应的唤醒方式为: sleep 睡眠时间结束。
    ● Object.wait(time) 方 法 , 对 应 的 唤 醒 方 式 为 : 调 用 Object.notify() /Object.notifyAll()去主动唤醒,或者限时结束。
    ● LockSupport.parkNanos(time)/parkUntil(time) 方法,对应的唤醒方式为:线程调用配套的 LockSupport.unpark(Thread)方法结束,或者线程停止(park)时限结束。
    进入 BLOCKED 状态、 WAITING 状态、 TIMED_WAITING 状态的线程都会让出 CPU 的使用权;另外,等待或者阻塞状态的线程被唤醒后,进入 Ready 状态,需要重新获取时间片才能接着运行。
    6. TERMINATED 状态
    线程结束任务之后,将会正常进入 TERMINATED(死亡)状态;或者说在线程执行过程中发生了异常(而没有被处理),也会导致线程进入死亡状态。

3 多线程实战

3.1 Thread方式实现多线程

操作步骤:

  1. 定义子类继承Thread类。
  2. 类中重写Thread类中的run方法。
  3. 创建Thread子类对象,即创建了线程对象。
  4. 调用线程对象start方法:启动线程,调用run方法。

案例实现:
启动一个线程,在线程中执行1-10000的偶数打印工作

public class MyThread extends Thread {

    @Override
    public void run() {
        for (int i = 1; i <= 10000; i++) {
            if (i % 2 == 0) {
                //Thread.currentThread().getName():得到线程的名字
                System.out.println(Thread.currentThread().getName() + "\t" + i);

            }
        }
    }
}

测试类

public class Test1 {

    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();
        myThread1.start();

        MyThread myThread2 = new MyThread();
        myThread2.start();

        System.out.println(Thread.currentThread().getName() + "  main 线程 over");

    }

}

main方法中可以增加下列代码来阻塞mainxiancheng

        JOptionPane.showMessageDialog(null, "是否确认向下执行...."); //主线程进入IO阻塞
        System.out.println("main over");

3.2 Runable方式实现多线程

操作步骤:

  1. 定义子类,实现Runnable接口。
  2. 类中重写Runnable接口中的run方法。
  3. 通过Thread类含参构造器创建线程对象。
  4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
  5. 调用Thread类的start方法:开启线程, 调用Runnable子类接口的run方法。

案例实现;

public class ThreadDemo {

    public static void main(String[] args) {
        //方式1:Thread子类,启动
        MyThread thread1 = new MyThread();
        thread1.start();

        //方式2:Runable方式(推荐的方式)
        //优势:可以实现多继承,比如继承BaseDao,然后再实现Runnable
        MyTask task = new MyTask();
        Thread thread2 = new Thread(task);
        thread2.start();

        //方式3:匿名内部类
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <= 100; i++) {
                    System.out.println(Thread.currentThread().getName() +  "\t" +  i);
                }
            }
        }).start();
    }

}

//方式1
class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() +  "\t" +  i);
        }
    }
}

//方式2
//优势:可以实现多继承,比如继承BaseDao,然后再实现Runnable
class MyTask implements Runnable {

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

3.3 Thread常见方法

  • 构造函数:
    • Thread(): 创建新的Thread对象
    • Thread(String threadname): 创建线程并指定线程实例名
    • Thread(Runnable target): 指定创建线程的目标对象,它实现了Runnable接口中的run方法
    • Thread(Runnable target, String name): 创建新的Thread对象
  • 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控制,使其他线程有机会被执行,时间到后重排队。
  • stop(): 强制线程生命期结束,不推荐使用
  • boolean isAlive(): 返回boolean,判断线程是否还活着
  • interrupt(): 发送中断信号(true)
    • 如果线程在阻塞状态,比如sleep(),join(),wait(),这时接收到中断信号会抛出一个异常InterruptException,同时中断信号清除(false)
    • 只是发送信号,不会对线程产生影响
  • static interrupted(): 得到中断信号(true),然后把中断信号设置成false
  • isInterrupted(): 得到中断信号,不会清除中断信号

案例演示

Thread.sleep()

指定线程休眠的时间,单位毫秒,让出cpu时间片,其他线程可以抢占cpu时间片

public class MyTask implements Runnable {

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                //Thread.currentThread().getName():得到线程的名字
                System.out.println(Thread.currentThread().getName() + "\t" + i);
                try {
                    Thread.sleep(1000);
                    Thread.yield();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }

    }
}

Thread.join()

当某个程序执行流中调用其他线程的 join() 方法时, 调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止

原理:
有两个线程1,2如下:

执行join()方法后:

演示案例:

public class Test1 {

    /**
     * CountDownLatch:可以实现相同的效果
     * @param args
     */
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 100; i++) {
                    System.out.println(Thread.currentThread().getName() + "\t" + i);
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        t1.start();

        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println(Thread.currentThread().getName() +  "  main orver");

    }

}

结果:

Thread.stop()

强制线程生命期结束,不推荐使用,线程退出方式粗暴,不管线程正在执行的任务,直接退出,可能丢失数据

演示案例:

public class Test1 {

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyTask());
        t1.start();

        Scanner in = new Scanner(System.in);
        System.out.println("输入1/0:0表示退出");
        int i = in.nextInt(); ///主线程进入IO阻塞

        if (i == 0) {
            t1.stop();
        }

        System.out.println("main over");
    }


    static class MyTask implements Runnable {

        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

}

Thread.interrupt()

发送中断信号(true),只是发送信号,不会对线程产生影响

演示案例:

public class Test1 {

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyTask());
        t1.start();

        JOptionPane.showMessageDialog(null, "是否确认向下执行...."); //主线程进入IO阻塞
        t1.interrupt(); //发送中断信号给t1

        System.out.println("main over");
    }

    static class MyTask implements Runnable {

        @Override
        public void run() {
            while (true) {
                System.out.println("a");

                try {
                    Thread.sleep(500000);
                } catch (InterruptedException e) {
                    System.out.println("bbbbbbbbbbbbbbb");
                    e.printStackTrace();
                    Thread.currentThread().interrupt(); //再次发送中断信号,中断信号发给阻塞线程,抛出Interrupt异常,中断信号清除
//                    throw new RuntimeException(e);
                }

                //得到中断信号,优雅的退出
                if (Thread.interrupted()) {
                    break;
                }

            }
        }
    }

}

3.4 线程优先级

  • MAX_PRIORITY:10 线程优先级最大值为10
  • MIN _PRIORITY:1 线程优先级最小为1
  • NORM_PRIORITY:5 默认线程优先级为5
    public static void main(String[] args) {
        MyTask myTask = new MyTask();
        Thread thread1 = new Thread(myTask, "t1");
        thread1.setPriority(Thread.MIN_PRIORITY);//设置线程优先级为1
        thread1.start();

        Thread thread2 = new Thread(myTask, "t2");
        thread1.setPriority(Thread.MAX_PRIORITY);//设置线程优先级为10
        thread2.start();
    }

3.5 守护线程

  • 其他线程都执行结束,守护线程自动结束
  • 守护启动子线程,也是守护线程
  • 守护线程的语法thread.(setDaemon(true)设置守护线程
public class Test2 {

    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();
//        myThread1.setDaemon(true);
        myThread1.start(); //守护线程, gc线程,jvm线程结束gc会自动结束
        JOptionPane.showMessageDialog(null, "是否确认向下执行...."); //主线程进入IO阻塞
        System.out.println("main over");
    }

}

class MyThread extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("a");
        }
    }
}

3.6 多线程自增i++和线程执行原理

        Plus plus = new Plus();
        PlusTask plusTask = new PlusTask(plus);

        Thread t1 = new Thread(plusTask);
        Thread t2 = new Thread(plusTask);
        Thread t3 = new Thread(plusTask);
        Thread t4 = new Thread(plusTask);

如上述代码,创建t1、t2、t3、t4 4个线程对象时同时引用相同线程操作对象plusTask,从而使得t1、t2、t3、t4 4个线程能够操作堆内存中线程所共享的Plus对象

下面是具体代码实现:
自增类

public class Plus {

    private int amount = 0;

    public void selfPlus() {
            amount ++;
    }

    public int getAmount() {
        return amount;
    }

}

自增任务类

public class PlusTask implements Runnable {

    private Plus plus;

    public PlusTask() {
    }

    public PlusTask(Plus plus) {
        this.plus = plus;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100000000; i++) {
            plus.selfPlus();
        }
    }
}

测试

    public static void main(String[] args) throws InterruptedException {
        Plus plus = new Plus();
        PlusTask plusTask = new PlusTask(plus);

        Thread t1 = new Thread(plusTask);
        Thread t2 = new Thread(plusTask);
        Thread t3 = new Thread(plusTask);
        Thread t4 = new Thread(plusTask);

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

        t1.join();
        t2.join();
        t3.join();
        t4.join();

        System.out.println("实际的值 = " + plus.getAmount());

    }

在上述代码中,4个线程自增一个堆(共享的)里的对象的值,即线程t1、t2、t3、t4同时操作amount进行自增操作,又因为自增操作在CPU中不属于原子性操作,从而导致了输出结果远小于理论结果。
自增反编译图:

4 线程同步

4.1 多窗口买票

4.1.1 实现思路

4.1.2 代码实现

Ticket类:

package l_ticket;

public class Ticket {

    private int count = 100;

    /**
     * 查票
     * @return
     */
    public int getCount() {
        return count;
    }

    public int out() {
        return this.count--;
    }

}

WindowTask类;

package l_ticket;

public class WindowTask implements Runnable {

    private Ticket ticket;

    public WindowTask() {
    }

    public WindowTask(Ticket ticket) {
        this.ticket = ticket;
    }

    @Override
    public void run() {
        while (true) {
            //票数小于0,窗口退出
            if (ticket.getCount() <= 0) {
                break;
            }

            //阻塞,会有超卖
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(Thread.currentThread().getName() + "\t" + ticket.out());
        }
    }
}

测试类:

package l_ticket;

public class Test1 {

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

        Thread window1 = new Thread(task, "1窗口");
        Thread window2 = new Thread(task, "2窗口");
        Thread window3 = new Thread(task, "3窗口");
        Thread window4 = new Thread(task, "4窗口");



        window1.start();
        window2.start();
        window3.start();
        window4.start();

    }

}

4.1.3 测试结果

1窗口	3
2窗口	2
3窗口	0
3窗口	1
4窗口	-1

出现卖出0、-1票的情况,即超卖问题

4.1.4 结果分析

理想状态如下:

极端状态:

  • 超卖问题的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
  • 解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行,即完成对临界区代码块的加锁
    • 临界区代码块:多个线程同时访问的代码块
    • 共享数据:临界区代码块中,多个线程共享访问的堆里面的数据
    • 加锁:通过共享对象给临界区代码块加锁

4.2 synchronized内置锁

4.2.1 概念

1. 临界区资源(共同数据)
表示一种可以被多个线程使用的公共资源或共享数据,但是每一次只能有一个线程使用它。一旦临界区资源被占用,想使用该资源的其他线程必须等待。
2. 临界区代码段(Critical Section)
是每个线程中访问临界资源的那段代码,多个线程必须互斥地对临界区资源进行访问。线程进入临界区代码段之前,必须在进入区申请资源,申请成功之后进行临界区代码段,执行完成之后释放资源。临界区代码段(Critical Section)的进入和退出、


在 Hotspot 虚拟机中, Monitor 是由 C++类ObjectMonitor 实现, ObjectMonitor 类定义ObjectMonitor.hpp 文件中,其构造器代码大致如下:


/Monitor 结构体
ObjectMonitor::ObjectMonitor() {
    _header = NULL;
    _count = 0;
    _waiters = 0,
    
    //线程的重入次数
    _recursions = 0;
    _object = NULL;
    
    //标识拥有该 monitor 的线程
    _owner = NULL;
    
    //等待线程组成的双向循环链表
    _WaitSet = NULL;
    _WaitSetLock = 0 ;
    _Responsible = NULL ;
    _succ = NULL ;
    
    //多线程竞争锁进入时的单向链表
    cxq = NULL ;
    FreeNext = NULL ;
    
    //_owner 从该双向循环链表中唤醒线程节点
    _EntryList = NULL ;
    _SpinFreq = 0 ;
    _SpinClock = 0 ;
    OwnerIsThread = 0 ;
}


4.2.2 synchronized语法

1. 代码块
synchronized (共享对象(plus,ticket)) {//临界区代码块
    //对共对象的访问(plus)(ticket)
}
2. 示例

synchronized(对象名):对共享对象加锁
多窗口售票

  synchronized (ticket) {
                //票数小于0,窗口退出
                if (ticket.getCount() <= 0) {
                    break;
                }

                //阻塞,会有超卖
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                System.out.println(Thread.currentThread().getName() + "\t" + ticket.out());
            }
}

自增1亿次
synchronized(this):对线程对象加锁

public class SynDemo1 {
    public static void main(String[] args) {
        Car a = new Car();
        Thread t1 = new Thread1(a);
        Thread t2 = new Thread2(a);
        t1.start();
        t2.start();
    }
}

class Thread1 extends Thread {
    private Car a;

    public Thread1(Car a) {
        this.a = a;
    }
    public void run() {
        a.fun1();
    }
}

class Thread2 extends Thread {
    private Car a;

    public Thread2(Car a) {
        this.a = a;
    }
    public void run() {
        a.fun2();
    }
}

class Car {
    public synchronized  void fun1() {
        //synchronized (this) {
            System.out.println("开始打蜡");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("打蜡结束");
        //}
    }

    public void fun2() {
        synchronized (this) {
            try {
                Thread.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("开始抛光");
            System.out.println("抛光结束");
        }
    }
}

syncronized(class):对静态方法加锁

public class SynDemo1 {
    public static void main(String[] args) {
        Car a = new Car();
        Thread t1 = new Thread1(a);
        Thread t2 = new Thread2(a);
        t1.start();
        t2.start();
    }
}

class Thread1 extends Thread {
    private Car a;

    public Thread1(Car a) {
        this.a = a;
    }
    public void run() {
        a.fun1();
    }
}

class Thread2 extends Thread {
    private Car a;

    public Thread2(Car a) {
        this.a = a;
    }
    public void run() {
        a.fun2();
    }
}

class Car {
    public synchronized static void fun1() {
            System.out.println("开始打蜡");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("打蜡结束");
    }

    public static void fun2() {
        synchronized (Car.class) {
            try {
                Thread.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("开始抛光");
            System.out.println("抛光结束");
        }
    }
}

4.2.3 使用注意事项

在使用syncronized时必须有相同的锁,在多个线程使用不同的锁时,syncronized不会起作用。
案例演示:

public class MyThread1 extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            synchronized (String.class) {
                System.out.println("a");

                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                System.out.println("b");
            }
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
}
public class MyThread2 extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            synchronized (Integer.class) {
                System.out.println("1");

                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                System.out.println("2");
            }
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
}

测试类

public class Test1 {

    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        MyThread2 t2 = new MyThread2();

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

}

4.3 死锁

synchronized嵌套时,比如张三在A电话亭想去B电话亭,李四在B电话亭想去A电话亭,这时会发生死锁。
a线程锁定一个资源,同时想获取b线程的资源,b线程锁定一个资源,同时想获取a线程的资源。
案例演示:

public class DeadLock {

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (String.class) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                    synchronized (Integer.class) {
                    }
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (Integer.class) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                    synchronized (String.class) {

                    }

                }
            }
        });


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

}

4.4 CountDownLatch

juc的公共锁

  • 构造函数new CountDownLatch(perms 10)授权数量
  • countDown():授权数量-1
  • await():执行后线程进入阻塞,知道授权数量=0
    案例演示:
    Plus类
public class Plus {

    private int count = 0;

    public void selfPlus() {
        count ++;
    }

    public int getCount() {
        return count;
    }
}

MyTask类

public class MyTask implements Runnable {

    Plus plus;
    CountDownLatch countDownLatch;

    public MyTask() {
    }

    public MyTask(Plus plus, CountDownLatch countDownLatch) {
        this.plus = plus;
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        synchronized (plus) {
            for (int i = 0; i < 100000000; i++) {
                plus.selfPlus();
            }
            countDownLatch.countDown(); //-1
        }
    }
}

测试类

public class Test1 {

    public static void main(String[] args) {

        Plus plus = new Plus();
        CountDownLatch countDownLatch = new CountDownLatch(4);
        MyTask task = new MyTask(plus, countDownLatch);

        for (int i = 0; i < 4; i++) {
            new Thread(task).start();
        }

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("理论值 = 400000000");
        System.out.println("实际值 = " + plus.getCount());
        System.out.println("理论值和实际值相差 = " + (400000000 - plus.getCount()));
    }

}

5 线程通讯

5.1 等待唤醒机制

线程的等待机制:


synchronized(内置锁)

  • EntryList:双向队列:等待锁的多列
  • Owner:正在执行的线程
  • WaitSet:双向队列
    • locko.wait:wait方法一定在synchronize代码块中,把当前线程放到waitset中
    • locko.notify:把waitset队列中的一个随机线程放到entrylist中
    • locko.notifyAll:把waitset队列中的所有线程都放到entrylist中
  • 所有对象都会关联一个c++结构,这个结构ObjectMonier

/Monitor 结构体
ObjectMonitor::ObjectMonitor() {
    _header = NULL;
    _count = 0;
    _waiters = 0,
    
    //线程的重入次数
    _recursions = 0;
    _object = NULL;
    
    //标识拥有该 monitor 的线程
    _owner = NULL;
    
    //等待线程组成的双向循环链表
    _WaitSet = NULL;
    _WaitSetLock = 0 ;
    _Responsible = NULL ;
    _succ = NULL ;
    
    //多线程竞争锁进入时的单向链表
    cxq = NULL ;
    FreeNext = NULL ;
    
    //_owner 从该双向循环链表中唤醒线程节点
    _EntryList = NULL ;
    _SpinFreq = 0 ;
    _SpinClock = 0 ;
    OwnerIsThread = 0 ;
}

5.2 生产者消费者模式

5.2.1 实现思路

  • 包子铺线程负责生产包子
  • 吃货线程负责消费包子
  • 包子对象

5.2.2 代码实现

1. 共享对象(Baozi)
public class Baozi {

    private String pier;
    private String xianer;
    private boolean flag = false ;//包子资源 是否存在  包子资源状态

    public String getPier() {
        return pier;
    }

    public void setPier(String pier) {
        this.pier = pier;
    }

    public String getXianer() {
        return xianer;
    }

    public void setXianer(String xianer) {
        this.xianer = xianer;
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    @Override
    public String toString() {
        return "Baozi{" +
                "pier='" + pier + '\'' +
                ", xianer='" + xianer + '\'' +
                ", flag=" + flag +
                '}';
    }
}
2. 生产者(Baozip)
public class Baozipu extends Thread {

    private Baozi baozi;

    public Baozipu() {
    }

    public Baozipu(Baozi baozi) {
        this.baozi = baozi;
    }

    @Override
    public void run() {
        int count = 0;
        while (true) {
            synchronized (baozi) {
                if (baozi.isFlag()) {
                    try {
                        baozi.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }

                if (count % 2 == 0) {
                    baozi.setPier("米皮");
                    baozi.setXianer("韭菜鸡蛋");
                } else {
                    baozi.setPier("面皮");
                    baozi.setXianer("猪肉大葱");
                }

                baozi.setFlag(true); //包子已经生成好了的标记
                System.out.println("包子已经生产完毕," + baozi);
                System.out.println("吃货来吃包子吧");
                count++;

                baozi.notify();
            }
        }
    }
}
3. 消费者(ChiHuo)
public class ChiHuo extends Thread {

    private Baozi baozi;

    public ChiHuo() {
    }

    public ChiHuo(Baozi baozi) {
        this.baozi = baozi;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (baozi) {
                if (!baozi.isFlag()) {
                    try {
                        baozi.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }

                System.out.println("吃货正在吃 " + baozi);
                baozi.setFlag(false);
                baozi.notify();
            }
        }
    }
}
4. 测试类
public class Test1 {

    public static void main(String[] args) {
        Baozi baozi = new Baozi();

        Baozipu baozipu = new Baozipu(baozi); //包子铺线程
        ChiHuo chiHuo = new ChiHuo(baozi); //吃货线程

        chiHuo.start();
        baozipu.start();
    }

}

6 juc显示锁

  • 内置锁:synchronized
  • 现实锁:juc锁

JDK5 版本引入了 java.util.concurrent 并发包,简称为 JUC 包,JUC 出自并发大师 Doug Lea 之手, Doug Lea 对 Java 并发性能的提升做出了巨大的贡献。

6.1 Lock接口

Java 对象锁还存在性能问题。在竞争稍微激烈的情况下, Java 对象锁会膨胀为重量级锁(基于操作系统的 Mutex Lock 实现),而重量级锁的线程阻塞和唤醒操作,需要进程在内核态和用户态之间来回切换,导致其性能非常低。JUC显示锁因为是纯粹Java语言实现,避免了这些问题,同时JUC显示锁具备了对象锁不具备的高级特性r如限时抢锁、可中断抢锁、多个等待队列。
显式锁不再作为 Java 内置特性来实现,而是作为 Java 语言可编程特性来实现

java.util.concurrent.locks.Lock接口的主要抽象方法如下:

方 法描 述
void lock()抢锁。 成功则向下运行,失败则阻塞抢锁线程
void lockInterruptibly() throws InterruptedException可中断抢锁,当前线程在抢锁的过程中可以响应中断信号
boolean tryLock()尝试抢锁, 线程为非阻塞模式,在调用 tryLock 方法后立即返回。抢锁成功返回 true, 抢锁失败返回 false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException限时抢锁,到达超时时间返回 false。并且此限时抢锁方法也可以响应中断信号
void unlock();释放锁
Condition newCondition();获取与显式锁绑定的 Condition 对象,用于“等待-通知”方式的线程间通信

从 Lock 提供的接口方法可以看出, 显式锁至少比 Java 内置锁多了以下优势:
1. 可中断获取锁
使用 synchronized 关键字获取锁的时候,如果线程没有获取到被阻塞,阻塞期间该线程是不响应中断信号(interrupt)的;而使用 Lock.lockInterruptibly( )方法获取锁时,如果线程被中断,线程将抛出中断异常。
2. 可非阻塞获取锁
使用 synchronized 关键字获取锁时,如果没有成功获取,线程只有被阻塞;而使用Lock.tryLock( )方法获取锁时,如果没有获取成功,线程也不会被阻塞,而是直接返回 false。
3. 可限时抢锁
使用 Lock.tryLock(long time, TimeUnit unit)方法, 显式锁可以设置限定抢占锁的超时时间。而在使用 synchronized 关键字获取锁时,如果不能抢到锁,线程只能无限制阻塞。

6.2 ReentrantLock的基本用法

ReentrantLock 是 Java 中的一个可重入锁实现,它可以用于多线程环境下对共享资源的互斥访问。 与内置的synchronized关键字相比,ReentrantLock 提供了更灵活和强大的锁定机制。

  • 可重入性:ReentrantLock 是可重入的,也就是说同一个线程可以多次获得该锁而不会导致死锁。这个特性可以避免在递归函数或相互调用的方法中出现死锁情况。

  • 公平性:ReentrantLock 可以选择是否公平地分配锁。在公平模式下,等待时间最长的线程将获得锁的访问权。在非公平模式下,默认是竞争方式获取锁。

  • 锁定和解锁:要获取锁,可以使用lock()方法,如果锁已经被其他线程持有,则当前线程会阻塞直到获得该锁。为了释放锁,应该使用unlock()方法。

  • 条件变量:ReentrantLock 提供了条件变量(Condition)的支持,通过条件变量可以实现更复杂的线程通信和同步机制。

案例演示:

public class ReenterLockTest implements Runnable {
    public static ReentrantLock lock = new ReentrantLock();
    public static int i = 0;

    public void run() {
        lock.lock();
        lock.lock();
        try {
            for (int j = 0; j < 10000000; j++) {
                i++;
            }
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReenterLockTest r1 = new ReenterLockTest();
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r1);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

6.3 读写锁

读写锁(Read-Write Lock)是一种并发控制机制,用于提高对共享资源的访问效率。它允许多个线程同时读取共享资源,但在进行写操作时需要独占访问,读写锁适用于读多写少的并发情况。

  • 多个线程可以同时获取读锁:当没有线程持有写锁时,多个线程可以同时获得读锁,并发读取共享资源。

  • 线程获得写锁时独占访问:如果有线程已经持有读锁或写锁,其他线程请求写锁时将被阻塞,直到当前线程释放写锁。

  • 读锁不排斥其他读锁:读锁之间不互斥,多个线程可以同时持有读锁并进行读取操作。

案例实现:

public class ReadWriteLockDemo {
    static CountDownLatch countDownLatch;

    private static Lock lock = new ReentrantLock();
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock = readWriteLock.readLock();
    private static Lock writeLock = readWriteLock.writeLock();
    private int value;

    public Object handleRead(Lock lock) throws InterruptedException {
        lock.lock(); //阻塞
        try {
            Thread.sleep(1000);
            System.out.println("read success");
            countDownLatch.countDown();
            return value;
        } finally {
            lock.unlock();
        }
    }

    public void handleWrite(Lock lock, int index) throws InterruptedException {
        lock.lock(); //阻塞
        try {
            Thread.sleep(1000);
            value = index;
            System.out.println("write success");
            countDownLatch.countDown();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final ReadWriteLockDemo demo = new ReadWriteLockDemo();
        Runnable readRunnable = new Runnable() {
            public void run() {
                try {
                    demo.handleRead(readLock);
//                    demo.handleRead(lock);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Runnable writeRunnable = new Runnable() {

            public void run() {
                try {
                    demo.handleWrite(writeLock, new Random().nextInt());
//                    demo.handleWrite(lock, new Random().nextInt());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        countDownLatch = new CountDownLatch(20);

        long t1 = System.currentTimeMillis(); //1970-1-1 0:0:0:0到现在的毫秒

        for (int i = 0; i < 18; i++) {
            new Thread(readRunnable).start();
        }

        for (int i = 18; i < 20; i++) {
            new Thread(writeRunnable).start();
        }

        countDownLatch.await();
        long t2 = System.currentTimeMillis(); //1970-1-1 0:0:0:0到现在的毫秒
        System.out.println(t2 - t1); //期待20s
    }
}

6.4 Semaphore许可管理器

Semaphore 是一个是许可管理器,可以用来控制在同一时刻访问共享资源的线程数量,Semaphore 维护了一组虚拟许可,其数量可以通过构造函数的参数指定。线程在访问共享资源前,必须使用 Semaphore 的 acquire 方法获得许可,如果许可数量为 0,该线程则一直阻塞。线程访问完成资源后,必须使用 Semaphore 的 release 方法去释放许可。

6.4.1 Semaphore 的主要方法

  • Semaphore(permits):构造一个 Semaphore 实例,初始化其管理的许可数量为 permits 参数值。
  • Semaphore(permits,fair):是否以公平模式(fair 参数是否为 true)进行许可的发放。
  • availablePermits( ):获取 Semaphore 对象可用的许可数量。
  • acquire( ):尝试获取 1 个许可。而当前线程被中断,则会抛出 InterruptedException 异常并终止阻塞
  • acquire(permits):尝试去阻塞的获取 permits 个许可
  • acquierUninterruptibly( ):阻塞的过程不可中断,直到成功获取 1 许可。
  • acquireUninterruptibly(permits):获取 permits 个许可,阻塞的过程不可中断
  • tryAcquire():非阻塞获取1个许可
  • tryAcquire(permits):非阻塞获取 permits 个许可。
  • tryAcquire(timeout,TimeUnit):限时获取许可
  • release( ):释放 1 个可用的许可
  • release(permits):释放 permits 个可用的许可。
  • drainPermits( ):当前线程获得剩余的所有可用许可。
  • hasQueuedThreads( ):判断当前 Semaphore 对象上是否存在正在等待许可的线程。
  • getQueueLength( ):获取当前 Semaphore 对象上正在等待许可的线程数量。

6.4.2 Semaphore 案例演示

假设有 10 个人在银行办理业务,只有 2 个工作窗口,使用 Semaphore 模拟银行排队,代码如下:

public class SemaphoreTest {

    public static void main(String[] args) throws InterruptedException {
        //线程池,用于多线程模拟测试
        final CountDownLatch countDownLatch = new CountDownLatch(10);
        //创建信号量,含有2个许可
        final Semaphore semaphore = new Semaphore(2);
        AtomicInteger index = new AtomicInteger(0);
        //创建Runnable可执行实例
        Runnable r = () ->
        {
            try
            {
                //抢占一个许可
                semaphore.acquire(1);

                //模拟业务操作: 处理排队业务
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                System.out.println(sdf.format(new Date()) + ", 受理处理中...,服务号: " + index.incrementAndGet());
                Thread.sleep(1000);
                //释放一个信号
                semaphore.release(1);
            } catch (Exception e)
            {
                e.printStackTrace();
            }
            countDownLatch.countDown();
        };


        //创建4条线程
        Thread[] tArray = new Thread[10];
        for (int i = 0; i < 10; i++)
        {
            tArray[i] = new Thread(r, "线程" + i);
        }
        //启动4条线程
        for (int i = 0; i < 10; i++)
        {
            tArray[i].start();
        }


        countDownLatch.await();
    }

}
  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值