Java学习之线程同步与线程间通信详解
作者:从未止步.. 时间:2022-05-27 08:03:59
线程同步的概念
由于同一个进程的多个线程共享同一块存储空间,在带来方便的同时,也会带来访问冲突的问题:
举例:
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();//启动消费者线程对象
}
}
输出:
消费者拿出----:香蕉
生产者放入+++++:苹果
消费者拿出----:苹果
生产者放入+++++:香蕉
消费者拿出----:香蕉
生产者放入+++++:苹果
消费者拿出----:苹果
生产者放入+++++:香蕉
消费者拿出----:香蕉
生产者放入+++++:苹果
消费者拿出----:苹果
生产者放入+++++:香蕉
从运行结果可以看出:生产者线程向缓冲区放入什么产品,消费者就从缓冲区中取出什么产品,生产者生产一个产品,消费者就消费一个产品,两者之间实现了通信.
来源:https://blog.csdn.net/m0_64365419/article/details/128437716