并发编程-java多线程总结

简介: 目录先了解几个概念1、线程的生命周期2、jvm内存模型3、线程的实现方式4、线程池5、保护措施5.1、 synchronized5.2、Lock&&ReadWriteLock5.3、 volatile先了解几个概念多线程:进程和线程是一对多的关系,一个进程(一个程序),由不同的线程来运行。

目录

先了解几个概念

  • 多线程:进程和线程是一对多的关系,一个进程(一个程序),由不同的线程来运行。有共享的空间也有独立的空间。
  • 并行: 同时进行,拿两个cpu来跑同样的程序同样的代码片段,那就并行了。
  • 并发:不同时进行,只有一个cpu,而多个线程都在争取这个cpu资源。便是并发。用TPS和QPS去衡量并发程度。
  • TPS:Transactions Per Second(每秒传输的事物处理个数),简单说就是服务器每秒处理事务的个数。
       完整的包括: 请求+数据库访问+响应
  • QPS:Queries Per Second(每秒查询率),简单说就是服务器每秒处理完请求的个数。

1、线程的生命周期

先了解线程的生命周期,上图。线程的生命周期从一个新的线程产生到结束中间会经历非常多的情况,大体上如下图,多线程环境下我们主要是再running的时候采取线程的保护措施,从而使多线程环境下,让线程进入阻塞的状态。这种保护思想其实就是排他了,到最后都得一个个来,无论式任务还是内存互不干扰,便达到线程安全了。

线程的生命周期
线程的生命周期

2、jvm内存模型

到了jdk8,内存模型已经有了相当的改变了,下图是小编学习了几篇优秀的博文学习,根据自己的理解绘制出来的,请多指教。

jdk8内存模型
jdk8内存模型
[1] [2] [3]

独立内存空间
  从图中可以看出线程安全的区域是在栈空间,每个线程会有独立的栈空间,从而也解释了为什么方法内是线程安全的,而全局变量这些是线程不安全的,因为这些都在堆区。

共享内存空间
  堆空间,和MateSpace是被所有线程共享的,因此在处理多线程问题的时候,其实主要是处理这两个空间的内容。共享区域在不加任何保护的情况下对其操作,会有异常结果。

怎么做到线程安全?

  • 只使用线程安全的内存空间,不使用共享的空间
  • 对共享的内存空间采取保护措施,比如:加Lock,volatile修饰等

3、线程的实现方式

  • 继承Thread
package com.example.demo;

import org.junit.Test;

/**
 * Project <demo-project>
 * Created by jorgezhong on 2018/8/31 16:01.
 */
public class ThreadDemo {

    @Test
    public void extendThreadTest() {
        ExtendThread extendThread = new ExtendThread();
        extendThread.start();
    }

    
    class ExtendThread extends Thread {

        @Override
        public void run() {
            // TODO: 2018/8/31
        }
    }

}
  • 实现Runnable接口
    @Test
    public void runnableThreadTest(){

        RunnableThread runnableThread = new RunnableThread();
        Thread thread = new Thread(runnableThread);
        thread.start();
        
    }

    class RunnableThread implements Runnable{

        @Override
        public void run() {
            // TODO: 2018/8/31
        }
    }
  • Callable和Future
@Test
    public void callableThreadTest(){

        CallableThread callableThread = new CallableThread();
        FutureTask<String> stringFutureTask = new FutureTask<>(callableThread);
        Thread thread = new Thread(stringFutureTask);
        thread.start();


    }

    /**
     * 这种实现是由返回值的
     */
    class CallableThread implements Callable<String>{

        @Override
        public String call() {
            // TODO: 2018/8/31
            return "";
        }
    }

补充:Fulture和Callable(Future模式)

首先,这两东西都在java.util.concurrent下,java本身就未多线程环境考虑了很多。看看下面的UML图,RunnableFuturej继承了Future和Runnable接口,将Future引入Runnable中,并且提供了默认实现FutureTask。RunnbleCallable和Future补充解决了两个问题,一个是多线程阻塞解决方案,另一个则是返回值问题。我们知道Runnable和Thread定义的run()是没有返回值的。而且当线程遇到IO阻塞的时候,只能等待,该线程无法做任何事情。Callable和Fulture分别解决了这两个问题。Callable提供了返回值的调用,而Fulture提供了多线程异步的机制。

Callable没什么好说的,例子如上面代码,就是多了个泛型的返回值,方法变成了call而已。Future就比较复杂了。FultureTask的构造方法接受Runnable或者Callable,也就是说Runnable和Callable的实例都可以使用Fulture来完成异步获取阻塞返回值的操作。

uml java fulture m
uml java fulture m

Future只有5个方法

  • cancel:取消任务的执行。参数表示是否立即中断任务
  • isCancelled:判断任务是否已经取消
  • isDone:判断任务是否已经完成
  • get():阻塞到任务接受获取返回值
  • get(long,TimeUnit):指定超时时间,获取返回值

Future模式缺陷
Fulture比较简单,基本上只通过两种方式:查看状态和等待完成。要么去查看一下是不是完成了,要么就等待完成,而线程和线程之间的通信只有通过等待唤醒机制来完成。原来的Fulture功能太弱,以至于google的Guava和Netty这些牛逼的框架都是重新去实现以拓展功能。而java8引入了实现了CompletionStage接口的CompletableFuture。可以说是极大的扩展了Future的功能。吸收了Guava的长处。

  • CompletableFuture介绍

关于CompletableFuture和的具体内容,后续再写一篇详细介绍。结合java8的Stream API CompletionStage接口定义很多流式编程的方法,我们可以进行流式编程,这非常适用于多线程编程。CompletableFuture实现了该接口,并拓展了自己的方法。对比Fulture多了几十个方法。大致可以分为同步的和异步的两种类型。而作业的时候,可以切入任务某一时刻,比如说完成后做什么。还可以组合CompletionStage,也就是进行线程之间的协调作业。

  • 使用线程池提交线程的实现(见下文)

4、线程池

我们可以看到java线程池相关的包,他们之间的关系如下图。

java uml thread
java uml thread
java uml thread m
java uml thread m

从uml类图可以看出(图片有点大,放大一下把),整个线程池构成其实是这样的:

  • 1、Executor封装了线程的实现
  • 2、Executor的子接口ExecutorService定义了管理Executor的一系列方法。
    ThreadPoolExecutor实现了ExecutorService,定义了一系列处理多线程的内容,比如线程工程和保存线程任务的队列
  • 3、ScheduledExecutorService扩展了ExecutorService,增加了定时任务调度的功能。
    ScheduledThreadPoolExecutor实现了ScheduledExecutorService,同时继承ThreadPoolExecutor的功能
  • 4、Executors静态类,包含了生成各种ExecutorService的方法。

从接口的组成可以看出,Executor、ExecutorService和ScheduledThreadPoolExecutor三个接口定义了线程池的基础功能。可以理解为他们三个就是线程池。
那么整个线程池是围绕两个默认实现ThreadPoolExecutor和ScheduledThreadPoolExecutor类来操作的。

至于操作,我发现java还蛮贴心的,默认实现的线程池只区分了可定时调度和不可定时调度的。实在是太过于灵活了,自己使用的话要配置一大堆参数,我想个线程池而已,给我搞这么多配置表示很麻烦,只需要关心是不是定时的,只考虑我分配多少线程给线程池就好了。因此有了Executors

Executors操作两个默认的实现类,封装了了大量线程池的默认配置,并提供了以下几种线程池给我们,我们只需要管线少部分必要的配置即可。

  • Single Thread Executor:只有一个线程的线程池,顺序执行
ExecutorService pool = Executors.newSingleThreadExecutor();
//提交实现到线程池
pool.submit(() -> {
    // TODO: 2018/8/31 do something
});
  • Cached Thread Pool:缓存线程池,超过60s池内线程没有被使用,则删掉。就是一个动态的线程池,我们不需要关心线程数
ExecutorService pool = Executors.newCachedThreadPool();
//提交实现到线程池
pool.submit(() -> {
    // TODO: 2018/8/31 do something
});
  • Fixed Thread Pool:固定数量的线程池
//参数为线程数
ExecutorService pool = Executors.newFixedThreadPool(8);
//提交实现到线程池
pool.submit(() -> {
    // TODO: 2018/8/31 do something
});
  • Scheduled Thread Pool:用于调度指定时间执行任务的线程池
//参数为线程数
ScheduledExecutorService pool = Executors.newScheduledThreadPool(8);

/*
* 提交到线程池
* 参数1:Runnable
* 参数2:初始延迟时间
* 参数3:间隔时间
* 参数4:时间单位
*/
pool.scheduleAtFixedRate(() -> {
    // TODO: 2018/8/31 do something 
}, 1000, 2000, TimeUnit.MILLISECONDS);
  • Single Thread Scheduled Pool:调度指定时间执行任务的线程池,只有一个线程
ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();

//参数少了初始延迟时间
pool.schedule(() -> {
    // TODO: 2018/8/31 do something 
}, 1000, TimeUnit.MILLISECONDS);

  • 线程池的配置策略

1、考虑业务类型
除了考虑计算机性能外,更多的还是考虑业务逻辑,如果业务是运算密集型的,不适合开太多的线程,因为运算一般是cpu在算,cpu本身就是用于计算,极快,因此一个线程很快就能计算完毕。线程多了反而增加了资源的消耗。另一种是IO密集型业务,这种业务就比较是适合开多一点线程,因为IO、通信这些业务本身就是非常慢的,大部分的系统的瓶颈都集中这两方面。因此这些业务适合开多个线程。

2、配合cpu的核心和线程数
在我们配置线程的时候,可以参考cpu的总线程,尽量不超出总线程数。一般使用核心数。


5、保护措施

5.1、 synchronized

这其实是一个监视器。可以监视类和对象。

原理:可以这么理解,每个实例化的对象都有一个公共的锁,该锁被该实例共享。因此对于该对象的所有被synchronized修饰的实例方法,是共享的同一个对象锁。同理,类锁也是一样的,伴随Class对象的生成,也会有一个类监视器,也就有一个默认的类锁了,被synchronized修饰的所有静态方法都共享一个类锁。

缺陷:同步锁关键子虽然方便,但是毕竟是被限制了修饰方式,因此不够灵活,另外修饰在方法上是修饰了整个方法,因此性能在并发量大且频繁的时候就显得不那么好了。

  • 修饰实例方法:
public synchronized void synchronizedMethod(){
    // TODO: 2018/8/29 do something 
}
  • 修饰静态方法:
public static synchronized void synchronizedMethod(){
    // TODO: 2018/8/29 do something
}
  • 修饰代码快:
public void synchronizedMethod(){
    //Object.class为锁对象,其实就是锁的钥匙,使用同一把钥匙的锁是同步的
    synchronized (Object.class){
        // TODO: 2018/8/29 do something
    }
}

5.2、Lock&&ReadWriteLock

由于synchronized的缺陷不够灵活,对应的自然有灵活的解决方案。Lock便是解决方案。Lock是java.util.concurrent.locks包下的一个接口。但是Lock是灵活了,但是既然都多线程了,我们当然是最求性能啦。由于很多数据是对查看没有线程安全要求的,只需要对写入修改要求线程安全即可,于是有了ReadWriteLock,读写锁可以只对某一方加锁,把锁住的内容范围更加缩小了,提升了性能。从下图可以看到,ReentrantLock实现了Lock而ReentrantReadWriteLock实现了ReadWiteLock。我们可以直接使用它们的实现类实现锁功能。

uml_java_lock
uml_java_lock

5.2.1、Lock

获取锁:lock()、tryLock()、lockInterruptibly()
释放锁:unLock()

直接上代码来学习效果是最快的[4]

  • DEMO:两个线程争取同一把锁
package com.example.demo;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.locks.ReentrantLock;

/**
 * Project <demo-project>
 * Created by jorgezhong on 2018/8/30 15:48.
 */
public class LockDemo {

    private static final Logger LOGGER = LoggerFactory.getLogger(LockDemo.class);

    /**
     * 两个线程争取同一把锁
     */
    @Test
    public void lockTest() throws InterruptedException {
        //造一把锁先
        ReentrantLock reentrantLock = new ReentrantLock();

        Thread thread0 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                lockTestHandle(reentrantLock);
            }
        });

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                lockTestHandle(reentrantLock);
            }
        });

        thread0.start();
        thread1.start();

        while (thread0.isAlive() || thread1.isAlive()) {}
    }

    private void lockTestHandle(ReentrantLock reentrantLock) {
        try {

            //  加锁
            reentrantLock.lock();
            LOGGER.info("拿到锁了,持有锁5s");
            Thread.sleep(5000);

        } catch (Exception e) {
            // TODO: 2018/8/30 do something
        } finally {
            // 记得自己释放锁,不然造成死锁了
            reentrantLock.unlock();
            LOGGER.info("释放锁了");
        }
    }



}


运行结果:我们可以看到,循环的代码是连续的,没有被其他线程干扰。确实是锁上了,使用同一个锁,必须等一个释放了另一个才能持有。一个线程持有锁,其他使用同一把锁的线程就会同步阻塞,重新持有锁之后才会结束阻塞的状态,才能往下执行代码。

16:36:05.740 [Thread-0] INFO com.example.demo.LockDemo - 拿到锁了
16:36:05.744 [Thread-0] INFO com.example.demo.LockDemo - 循环:0 持有锁
16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 循环:1 持有锁
16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 循环:2 持有锁
16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 循环:3 持有锁
16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 循环:4 持有锁
16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 释放锁了
16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 拿到锁了
16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 循环:0 持有锁
16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 循环:1 持有锁
16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 循环:2 持有锁
16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 循环:3 持有锁
16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 循环:4 持有锁
16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 释放锁了
16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 拿到锁了
16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 循环:0 持有锁
16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 循环:1 持有锁
16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 循环:2 持有锁
16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 循环:3 持有锁
16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 循环:4 持有锁
16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 释放锁了
16:36:05.747 [Thread-0] INFO com.example.demo.LockDemo - 拿到锁了

......
16:36:05.748 [Thread-1] INFO com.example.demo.LockDemo - 循环:4 持有锁
16:36:05.748 [Thread-1] INFO com.example.demo.LockDemo - 释放锁了
16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 拿到锁了
16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 循环:0 持有锁
16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 循环:1 持有锁
16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 循环:2 持有锁
16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 循环:3 持有锁
16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 循环:4 持有锁
16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 释放锁了

  • DEMO:可被中断锁
 /**
     * lockInterruptibly:加了可中断锁的线程,如果在获取不到锁,可被中断。
     * <p>
     * 中断其实是使用了异常机制,当调用中断方法,会抛出InterruptedException异常,捕获它可处理中断逻辑
     */
    @Test
    public void lockInterruptiblyTest() throws InterruptedException {

        ReentrantLock reentrantLock = new ReentrantLock();

        Thread thread0 = new Thread(() -> {

            try {
                lockInterruptiblyTestHandle(reentrantLock);
            } catch (InterruptedException e) {
                LOGGER.info("被中断了");
            }

        });

        Thread thread1 = new Thread(() -> {

            try {
                lockInterruptiblyTestHandle(reentrantLock);
            } catch (InterruptedException e) {
                LOGGER.info("被中断了");
            }

        });
        thread1.setPriority(10);

        thread1.start();
        thread0.start();

        Thread.sleep(500);
        thread0.interrupt();

        while (thread0.isAlive() || thread1.isAlive()) {}
    }

    private void lockInterruptiblyTestHandle(ReentrantLock reentrantLock) throws InterruptedException {
        /*
         * 加锁不能放在try...finally块里面,会出现IllegalMonitorStateException,意思是当lockInterruptibly()异常的时候,执行了unlock()方法
         * 其实就是加锁都抛出异常失败了,你还去解锁时不行的。放外面抛出异常的时候就不会去解锁了
         */
        reentrantLock.lockInterruptibly();
        try {
            LOGGER.info("拿到锁了,持有锁5秒");
            Thread.sleep(5000);
        } finally {
            // 释放锁
            reentrantLock.unlock();
            LOGGER.info("释放锁了");
        }
    }


从结果可以看到,thread-0被中断了之后不再继续执行


20:11:22.227 [Thread-1] INFO com.example.demo.LockDemo - 拿到锁了,持有锁5秒
20:11:22.742 [Thread-0] INFO com.example.demo.LockDemo - 被中断了
20:11:27.231 [Thread-1] INFO com.example.demo.LockDemo - 释放锁了

Process finished with exit code 0

5.2.2 ReadWriteLock

ReadWriteLock[4]只是定义了读锁和写锁两个方法,其具体实现和拓展再默认实现ReentrantReadWriteLock中。简单来说读写锁呢,提供读锁和写锁,将读和写要获取的锁类型分开,用一个对列来管理,所有的锁都会经过队列。当需要获取写锁的时候,后买的读写锁获取都需要等待,知道该写锁被释放才能进行。

    @Test
    public void readWriteLockTest(){

        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

        ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        try {
            readEvent();
        } catch (Exception e) {
            LOGGER.error(e.getMessage(),e);
        }finally {
            readLock.unlock();
        }

        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
        try {
            writeEvent();
        } catch (Exception e) {
            LOGGER.error(e.getMessage(),e);
        }finally {
            writeLock.unlock();
        }


    }

    private void writeEvent() {
        // TODO: 2018/9/3 done write event
    }

    private void readEvent() {

        // TODO: 2018/9/3 done read event

    }

总的来说:凡是遇到写,阻塞后面的线程队列,读与读是不阻塞的。

5.3、 volatile

volatile可修饰成员变量,能保证变量的可见性,但是不能保证原子性,也就是说并发的时候多个线程对变量进行计算的话,结果是会出错的,保证可见性只是能保证每个线程拿到的东西是最新的。

对于volatile来说,保证线程共享区域内容的可见性可以这么来理解,堆内存的数据原来是需要拷贝到栈内存的,相当于复制一份过去,但是呢。再不加volatile的时候,栈区计算完之后在赋值给堆区,问题就产生了。加了volatile之后,线程访问堆区的数据之后,堆区必须等待,知道栈区计算完毕将结果返回给堆区之后,其他线程才能继续访问堆区数据。

public volatile String name = "Jorgezhong";


  1. Java8内存模型

  2. Metaspace整体介绍

  3. jdk8 HotSpot内存模型

  4. Java并发编程:Lock

目录
相关文章
|
5天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
4天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
|
4天前
|
Java 开发者
Java多线程编程的艺术与实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的技术文档,本文以实战为导向,通过生动的实例和详尽的代码解析,引领读者领略多线程编程的魅力,掌握其在提升应用性能、优化资源利用方面的关键作用。无论你是Java初学者还是有一定经验的开发者,本文都将为你打开多线程编程的新视角。 ####
|
3天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
9天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
32 9
|
6天前
|
安全 Java 开发者
Java多线程编程中的常见问题与解决方案
本文深入探讨了Java多线程编程中常见的问题,包括线程安全问题、死锁、竞态条件等,并提供了相应的解决策略。文章首先介绍了多线程的基础知识,随后详细分析了每个问题的产生原因和典型场景,最后提出了实用的解决方案,旨在帮助开发者提高多线程程序的稳定性和性能。
|
10天前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
12天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
9天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
12天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
26 3
下一篇
无影云桌面