Java学习之线程同步与线程间通信详解

 更新时间:2022年12月27日 16:25:43   作者:从未止步..  
这篇文章主要为大家详细介绍了线程同步和线程之间的通信的相关知识,文中的示例代码讲解详细,对我们学习Java有一定的帮助,感兴趣的可以了解一下

线程同步的概念

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

举例:

public class Runnable_test implements Runnable {//实现Runnable接口
private  int ticknumbers=10;

    @Override
    public void run() {
        while(true){
            if(ticknumbers<=0){
                break;
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"-->拿到了第"+ticknumbers--+"票");//currentThread()监测线程的状态
        }
    }

    public static void main(String[] args) {
        Runnable_test runnable_test=new Runnable_test();
        new Thread(runnable_test,"小明").start();
        new Thread(runnable_test,"小黄").start();
        new Thread(runnable_test,"小红").start();
    }
}

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

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

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

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

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

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

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

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

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

同步代码块

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

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

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

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

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

对上述实例进行修改:

package Runnable;

public class Runnable_test implements Runnable {//实现Runnable接口
    private int ticknumbers = 20;
    private Object obj = new Object();//被加锁的对象,同步对象

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (ticknumbers > 0) {
                    System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticknumbers-- + "票");//currentThread()监测线程的状态
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else
                    break;
            }
        }
    }
}


class test{
    public static void main(String[] args) {
        Runnable_test runnable_test=new Runnable_test();
        new Thread(runnable_test,"小明").start();
        new Thread(runnable_test,"小黄").start();
        new Thread(runnable_test,"小红").start();
    }
}

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

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

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

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

同步方法

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

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

[权限访问限定]    synchronized 方法返回值 方法名称(参数列表){
//.............需要同步的代码,对共享资源的访问
}

package Runnable;

public class Runnable_test implements Runnable {//实现Runnable接口
    private int ticknumbers = 20;
    @Override
    public  void run() {
        while (true) {
            if (ticknumbers > 0) {
                ticks();//调用同步方法
            }
        else
            break;
        }
    }
    
    //同步方法
    public synchronized void ticks(){
        if (ticknumbers > 0) {
            System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticknumbers-- + "票");
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//测试类
class test{
    public static void main(String[] args) {
        Runnable_test runnable_test=new Runnable_test();
        new Thread(runnable_test,"小明").start();
        new Thread(runnable_test,"小黄").start();
        new Thread(runnable_test,"小红").start();
    }
}

输出:

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

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

package Runnable;

public class Runnable_test implements Runnable {//实现Runnable接口
    private int ticknumbers = 20;
    boolean tag = false;//设置此变量的作用是为了让一个线程进入同步块,另一个线程进入同步方法

    @Override
    public void run() {
        if(tag){
            while(true)
                ticks();
        }
        else{
            while (true) {
                synchronized (this) {
                    if (ticknumbers > 0) {
                        System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticknumbers-- + "票");//currentThread()监测线程的状态
                        try {
                            Thread.sleep(300);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else
                        break;
                }
            }
        }
    }

    //同步方法
    public synchronized void ticks() {
        if (ticknumbers > 0) {
            System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticknumbers-- + "票");
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        else
            return;
    }
}


    //测试类
    class test {
        public static void main(String[] args) throws InterruptedException {
            Runnable_test runnable_test = new Runnable_test();
            Thread thread1=new Thread(runnable_test, "小明");
            thread1.start();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            runnable_test.tag=true;
            Thread thread2=new Thread(runnable_test, "小黄");
            thread2.start();
        }
    }

输出:

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

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

由于同步代码块是给 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();//启动消费者线程对象
    }
}

输出:

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

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

以上就是Java学习之线程同步与线程间通信详解的详细内容,更多关于Java线程的资料请关注脚本之家其它相关文章!

相关文章

  • 浅析Java中的WeakHashMap

    浅析Java中的WeakHashMap

    这篇文章主要介绍了浅析Java中的WeakHashMap,WeakHashMap其实和HashMap大多数行为是一样的,只是WeakHashMap不会阻止GC回收key对象,那么WeakHashMap是怎么做到的呢,这就是我们研究的主要问题,需要的朋友可以参考下
    2023-09-09
  • SpringBoot Druid配置过程图解

    SpringBoot Druid配置过程图解

    这篇文章主要介绍了SpringBoot Druid配置过程图解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-03-03
  • 如何解决java:错误:无效的源发行版:17问题

    如何解决java:错误:无效的源发行版:17问题

    这篇文章主要介绍了如何解决java:错误:无效的源发行版:17问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-07-07
  • 一篇文章带你了解Maven的继承和聚合

    一篇文章带你了解Maven的继承和聚合

    这篇文章主要为大家介绍了Maven的继承和聚合,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-01-01
  • java编写的文件管理器代码分享

    java编写的文件管理器代码分享

    本文给大家分享的是一则使用java编写的文件管理器的代码,新人练手的作品,逻辑上还是有点小问题,大家帮忙看看吧。
    2015-04-04
  • Java 自动安装校验TLS/SSL证书

    Java 自动安装校验TLS/SSL证书

    这篇文章主要介绍了Java 自动安装校验TLS/SSL证书的示例,帮助大家更好的理解和使用Java,感兴趣的朋友可以了解下
    2020-10-10
  • java:抽象类与模板方法模式详解

    java:抽象类与模板方法模式详解

    这篇文章主要介绍了Java抽象类的构造模板模式用法,结合实例形式分析了java使用抽象类构造模板模式相关操作技巧,需要的朋友可以参考下
    2021-09-09
  • java 数据库连接与增删改查操作实例详解

    java 数据库连接与增删改查操作实例详解

    这篇文章主要介绍了java 数据库连接与增删改查操作,结合实例形式详细分析了java使用jdbc进行数据库连接及增删改查等相关操作实现技巧与注意事项,需要的朋友可以参考下
    2019-11-11
  • 关于Java整合RabbitMQ实现生产消费的7种通讯方式

    关于Java整合RabbitMQ实现生产消费的7种通讯方式

    这篇文章主要介绍了关于Java整合RabbitMQ实现生产消费的7种通讯方式,消息中间件是基于队列与消息传递技术,在网络环境中为应用系统提供同步或异步、可靠的消息传输的支撑性软件系统,需要的朋友可以参考下
    2023-05-05
  • java并发编程_线程池的使用方法(详解)

    java并发编程_线程池的使用方法(详解)

    下面小编就为大家带来一篇java并发编程_线程池的使用方法(详解)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-05-05

最新评论