java之线程同步和线程之间的通信

简介: java之线程同步和线程之间的通信

线程同步的概念:

由于同一个进程的多个线程共享同一块存储空间,在带来方便的同时,也会带来访问冲突的问题

举例:

package Runnable;
public class sync implements Runnable{
    private int ticks=5;
    @Override
    public void run() {
        while(true){
            if(ticks>0){
                System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticks-- + "票");
            }
            else{
                break;
            }
        }
    }
    public static void main(String[] args) {
        //创建一个售票对象
        sync sync=new sync();
        //创建三个线程对象,共享一个售票对象
        new Thread(sync,"小黄").start();
        new Thread(sync,"大军").start();
        new Thread(sync,"小明").start();
    }
}

在输出的数据中,显然出现了,一张票同时被大于1人拿到的情况,这与我们的现实显然不相符合。


为了解决此问题,Java 语言提供专门的机制来避免同一个对象被多个线程同时访问,这个机制就是线程同步。


当两个或多个线程同时访问同一个变量,并且有线程需要修改这个变量时,就必须采用同步的机制对其进行控制,否则就会出现逻辑错误的运行结果

造成上述这种错误逻辑结果的原因是:可能有多个线程取得的是同一个值,各自修改并存入,从而造成修改慢的后执行的线程把执行快的线程的修改结果覆盖掉了

因为线程在执行过程中不同步,多个线程在访问同一资源时,需要进行同步操作,被访问的资源称为共享资源。


同步的本质是加锁,Java 中的任何一个对象都有一把锁以及和这个锁对应的等待队列,当线程要访问共享资源时,首先要对相关的对象进行加锁


如果加锁成功,线程对象才能访问共享资源并且在访问结束后,要释放锁:如果加锁不成功,那么线程进入被加锁对象对应的是等待队列。


Java用synchronized关键字给针对共享资源进行操作的方法加锁。每个锁只有一把钥匙,只有得到这把钥匙之后才可以对被保护的资源进行操作,而其他线程只能等待,直到拿到这把钥匙。


实现同步的具体方式有同步代码块和同步方法两种


同步代码块:

使用 synchronized 关键字声明的代码块称为同步代码块

在任意时刻,只能有一个线程访问同步代码块中的代码,所以同步代码块也称为互斥代码块

同步代码块格式如下所示:

synchronized(同步对象){
//需要同步的代码,对共享资源的访问
}

synchronized关键字后面括号内的对象就是被加载的对象,同步代码块要实现对共享资源的访问

对上述实例进行修改:

package Runnable;
public class sync implements Runnable{
    private int ticks=5;
    private Object object=new Object();
    @Override
    public void run() {
            while (true) {
                synchronized (object) {
                if (ticks > 0) {
                    System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticks-- + "票");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }
        }
    }
    public static void main(String[] args) {
        //创建一个售票对象
        sync sync=new sync();
        //创建三个线程对象,共享一个售票对象
        new Thread(sync,"小黄").start();
        new Thread(sync,"大军").start();
        new Thread(sync,"小明").start();
    }
}

将票数产生变化的代码块修改为同步代码块:

修改过后输出,我们发现,并未出现同一张票,被第二个甚至第三个人拿到的情况:

多次运行,结果可能会不相同

小黄-->拿到了第5票
小黄-->拿到了第4票
大军-->拿到了第3票
大军-->拿到了第2票
大军-->拿到了第1票

在上面的修改中,仅仅是将需要互斥的代码放人了同步块中。此时,在抽票的过程中通过给同一个 obj对象加锁来实现互斥,从而保证线程的同步执行。

同步方法:

synchronized关键字也可以出现在方法的声明部分,该方法称为同步方法

当多个线程对象同时访问共享资源时,只有获得锁对象的线程才能进入同步方法执行,其他访问共享资源的线程将会进入锁对象的等待队列,执行完同步方法的线程会释放锁。

[权限访问限定]  synchronized 方法返回值 方法名称(参数列表){
//.............需要同步的代码,对共享资源的访问
}
package Runnable;
public class sync implements Runnable{
    private int ticks=5;
    @Override
    public void run() {
            while (true) {
                if(ticks>0){//若当前票数满足,则调用同步方法
                    show_ticks();
                }
                else{
                    break;
                }
        }
    }
    public synchronized void show_ticks() {
                if (ticks > 0) {
                    System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticks-- + "票");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
    public static void main(String[] args) {
        //创建一个售票对象
        sync sync=new sync();
        //创建三个线程对象,共享一个售票对象
        new Thread(sync,"小黄").start();
        new Thread(sync,"大军").start();
        new Thread(sync,"小明").start();
    }
}

输出:

小黄-->拿到了第5票
小黄-->拿到了第4票
大军-->拿到了第3票
大军-->拿到了第2票
小明-->拿到了第1票

同步方法的本质也是给对象加锁,但是是给同步方法所在类的 this 对象加锁,所以在上述实例中,我们就删除了obj对象的定义。

package Runnable;
public class sync implements Runnable{
    private int ticks=20;
    boolean tag=false;
    @Override
    public void run() {
        if (tag) {
            while (true)
                show_ticks();//进入同步方法
        } else {
            while (true) {
                synchronized (this) {
                    if (ticks > 0) {
                        System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticks-- + "票");
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    else{
                        return;
                    }
                }
            }
        }
    }
    public synchronized void show_ticks() {
        if (ticks > 0) {
            System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticks-- + "票");
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        else{
            return ;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        sync sync=new sync();
        new Thread(sync,"小黄").start();//小黄进入同步代码块
        Thread.sleep(1000);
        sync.tag=true;//将tag变量设置为true---->大军进入同步方法
        new Thread(sync,"大军").start();
    }
}

输出:

小黄-->拿到了第20票
小黄-->拿到了第19票
小黄-->拿到了第18票
小黄-->拿到了第17票
小黄-->拿到了第16票
小黄-->拿到了第15票
小黄-->拿到了第14票
小黄-->拿到了第13票
小黄-->拿到了第12票
小黄-->拿到了第11票
小黄-->拿到了第10票
小黄-->拿到了第9票
小黄-->拿到了第8票
小黄-->拿到了第7票
小黄-->拿到了第6票
小黄-->拿到了第5票
小黄-->拿到了第4票
大军-->拿到了第3票
大军-->拿到了第2票
大军-->拿到了第1票

通过程序运行结果可以看出:线程小黄执行同步代码块,线程大军执行同步方法,两个线程之间形成了同步。


由于同步代码块是给 this对象加锁,所以表明同步方法也是给 this对象加锁,否则,两者之间不能形成同步。


注意:多线程的同步程序中,不同的线程对象必须给同一个对象加锁,否则这些线程对象之间无法实现同步

线程组:

线程组可以看作是包含了许多线程的对象集,它拥有一个名字以及一些相关的属性,可以当作一个组来管理其中的线程。

每个线程都是线程组的一个成员,线程组把多个线程集成一个对象,通过线程组可以同时对其中的多个线程进行操作。在生成线程时必须将线程放到指定的线程组,也可以放在缺省的线程组中,缺省的就是生成该线程的线程所在的线程组。一旦一个线程加入了某个线程组,就不能被移出这个组。


java,lang包的ThreadGroup类表示线程组,在创建线程之前,可以创建一个ThreadGroup对象。

下面代码是创建线程组并在其中加人两个线程

ThreadGroup myThreadGroup = new ThreadGroup("a"); //创建线程组
//将下述两个线程加入其中
Thread myThread1 = new Thread(myThreadGroup,"worker1");
Thread myThread2 = new Thread(myThreadGroup,"worker2");
myThread1.start();
myThread2.start();

线程组的相关方法:

String getName(); //返回线程组的名字
ThreadGoup getParent(); //返回父线程
int tactiveCount(); //返回线程组中当前激活的线程的数目,包括子线程组中的活动线程
int enumerate(Thread list[])  //将所有线程组中激活的线程复制到一个线程数组中
void setMaxPriority(int pri)  //设置线程的最高优先级,pri是该线程组的新优先级
void interrupt()  //向线程组及其子组中的线程发送一个中断信息
boolean isDaemon()  //判断是否为Daemon线程组
boolean parentOf(ThreadGoup g)  //判断线程组是否是线程g或g的子线程
toString()  //返回一个表示本线程组的字符串 

线程组对象的基本应用:

举例:

package Runnable;
public class MyThreadgroup {
    public void test(){
        ThreadGroup threadGroup=new ThreadGroup("test");    //创建名为test的线程组
        Thread A=new Thread(threadGroup,"线程A");
        Thread B=new Thread(threadGroup,"线程B");
        Thread C=new Thread(threadGroup,"线程C");
        //为线程设置优先级
        A.setPriority(6);
        C.setPriority(4);
        A.start();
        B.start();
        C.start();
        System.out.println("threadGroup正在进行活动的个数:"+threadGroup.activeCount());
        System.out.println("线程A的优先级:"+A.getPriority());
        System.out.println("线程B的优先级:"+B.getPriority());
        System.out.println("线程C的优先级:"+C.getPriority());
    }
}
class MyThreadgroup_test{
    public static void main(String[] args) {
       MyThreadgroup myThreadgroup=new MyThreadgroup();
       myThreadgroup.test();
    }
}

输出:

threadGroup正在进行活动的个数:3
线程A的优先级:6
线程B的优先级:5
线程C的优先级:4

线程间的通信:

某些情况下,多个线程之间需要相互配合来完成一件事情,这些线程之间就需要进行通信”,把一方线程的执行情况告诉给另一方线程。

“通信”的方法在 java.lang.Object类中定义了,我们可以通过“生产者-消费者”模型来理解线程间的通信。


有两个线程对象,其中一个是生产者,另一个是消费者。生产者线程负责生产产品并放入产品缓冲区,消费者线程负责从产品缓冲区取出产品并消费。


当生产者线程获得 CPU 使用权后:


先判断产品缓冲区是否有产品,如果有产品就调用 wait()方法进入产品缓冲区对象的等待队列并释放产品缓冲区对象的锁;如果发现产品缓冲区中没有产品,就生产产品并放入缓冲区并调用notify()方法发送通知给消费者线程。


当消费者线程获得CPU使用权后:


先判断产品缓冲区是否有产品,如果有产品就拿出来消费并调用 notify()方法发送通知给生产者线程;如果发现产品缓冲区中没有产品,调用 wait()方法进入产品缓冲区对象的等待队列并释放产品缓冲区对象的锁。


注意:线程间通信是建立在线程同步基础上的,所以wait()notify()和notifyAll()方法的调用要出现在同步代码块或同步方法中


线程通信简单应用:

package Runnable;
 class Box {//产品缓冲区
    public String name="苹果";//表示产品的名称
    public boolean isFull=true;//表示当前缓冲区中是否有产品
}
//定义消费者类
class Cossumer implements Runnable {
    Box box;
    Cossumer(Box box) {
        this.box = box;
    }
    @Override
    public void run() {
        while (true) {
            synchronized (box) {//对产品缓冲区对象加锁
                if (box.isFull == true) //缓冲区中有产品
                {
                    System.out.println("消费者拿出----:" + box.name);
                    box.isFull = false;//设置缓冲区中产品为空
                    box.notify();//发送通知给生产者线程对象
                } else {
                    try {
                        //消费者线程进入产品缓冲区的等待队列并释放锁
                        box.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
    //生产者类
    class product implements Runnable{
        Box box;
        int Count=0;
        public product(Box box) {
            this.box=box;
        }
        @Override
        public void run() {
            while(true){
                synchronized (box)//对产品缓冲区对象加锁
                {
                    if(box.isFull==true)//缓冲区中有产品
                    {
                        try {
                            box.wait();//生产者线程进入等待队列并释放锁
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    else {
                        if (Count == 0) {
                            box.name = "香蕉";
                            System.out.println("生产者放入+++++:" + box.name);
                        } else {
                            box.name = "苹果";
                            System.out.println("生产者放入+++++:" + box.name);
                        }
                        Count=(Count+1)%2;//确保“香蕉和苹果都能被拿到”
                        box.isFull=true;//设置缓冲区中有产品
                        box.notify();//发送通知给消费者线程对象
                    }
                }
            }
        }
    }
class box_test{
    public static void main(String[] args) {
        Box box=new Box();//创建产品缓冲区对象
        product product=new product(box);
        Cossumer cossumer=new Cossumer(box);//生产者和消费者对象要共享同一个产品缓冲区
        Thread thread1=new Thread(product);//创建生产者线程对象
        Thread thread2=new Thread(cossumer);//创建消费者线程对象
        thread1.start();//启动生产者线程对象
        thread2.start();//启动消费者线程对象
    }
}

输出:

消费者拿出----:香蕉
生产者放入+++++:苹果
消费者拿出----:苹果
生产者放入+++++:香蕉
消费者拿出----:香蕉
生产者放入+++++:苹果
消费者拿出----:苹果
生产者放入+++++:香蕉
消费者拿出----:香蕉
生产者放入+++++:苹果
消费者拿出----:苹果
生产者放入+++++:香蕉

从运行结果可以看出:生产者线程向缓冲区放入什么产品,消费者就从缓冲区中取出什么产品,生产者生产一个产品,消费者就消费一个产品,两者之间实现了通信

相关文章
|
3天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
26 6
|
16天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
18天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
11天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
11天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
32 3
|
12天前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
17天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
58 6
|
16天前
|
监控 Java 数据库连接
Java线程管理:守护线程与用户线程的区分与应用
在Java多线程编程中,线程可以分为守护线程(Daemon Thread)和用户线程(User Thread)。这两种线程在行为和用途上有着明显的区别,了解它们的差异对于编写高效、稳定的并发程序至关重要。
26 2
|
16天前
|
监控 Java 开发者
Java线程管理:守护线程与本地线程的深入剖析
在Java编程语言中,线程是程序执行的最小单元,它们可以并行执行以提高程序的效率和响应性。Java提供了两种特殊的线程类型:守护线程和本地线程。本文将深入探讨这两种线程的区别,并探讨它们在实际开发中的应用。
23 1
|
17天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
52 1