【线程】使用同步:解决多线程引发的内存一致性错误问题

首先,需要理解一些理论上的东西。

 

多个线程并发对同一个资源进行操作,很可能发生内存一致性错误

 

究其原因,线程很多情况下对资源的操作不是原子的,这些代码会被分为若干条指令去执行,而在一个CPU时间片内又不能将这些指令全部执行完毕。

当多个线程同时操作同一个共享资源时,线程B拿着线程A的半成品进行操作,内存一致性错误就发生了。

 

如何解决?

1.同步,即:加锁。通过加锁的方式,可以确保唯一获取锁的线程可以不受干扰的执行完自己的程序片段。只有当前线程释放锁之后,其它线程才能对此资源进行操作。否则,只能等待当前线程执行完毕后释放锁(等待锁的线程全部被阻塞了);

 

2.资源不可变,即:只读。只读意味着数据不会变化,因此对只读数据的操作不可能发生内存不一致问题。

 

同步锁

同步机制的建立是基于其内部一个叫内部锁或者监视锁的实体。

内部锁在同步机制中起到两方面的作用:

对一个对象的排他性访问;

建立一种happens-before关系,而这种关系正是可见性问题的关键所在。

每个对象都有一个与之关联的内部锁。
当一个线程需要排他性的访问一个对象的域时,首先请求该对象的内部锁,当访问结束时释放内部锁。
在线程获得内部锁到释放内部锁的这段时间里,只有当前线程拥有这个内部锁。
当一个线程拥有一个内部锁时,其他线程将无法获得该内部锁。其他线程如果去尝试获得该内部锁,则会被阻塞。
当线程释放其拥有的内部锁时,该操作和对该锁的后续请求间将建立happens-before关系。

 

 

同步(synchronized)的两种方式:

1. 同步方法

非静态方法-this

当线程调用一个同步方法时,它会自动请求该方法所在对象的内部锁。
当方法返回结束时则自动释放该内部锁,即使退出是由于发生了未捕获的异常,内部锁也会被释放。
静态方法-Class
当调用一个静态的同步方法时,由于静态方法是和类(而不是对象)相关的,所以线程会请求类对象(Class Object)的内部锁。
因此用来控制类的静态域访问的锁不同于控制对象访问的锁。

 

2. 同步代码块

同步块必须指定所请求的是哪个对象的内部锁

可以实现更细粒度的控制

 

可重入同步

一个线程不能获得其他线程所拥有的锁,但是它可以获得自己已经拥有的锁。
允许一个线程多次获得同一个锁实现了可重入同步。
避免了线程自己阻塞自己。

 

 

线程同步可以解决内存不一致错误,但也引入了其它的问题

死锁、饥饿、活锁

 

A.线程死锁

两个或多个线程永久阻塞,互相等待对方释放资源。

 

package org.thread;
public class DeadLock {
	
	public static void main(String[] args) {
		final Friend zs = new Friend("zs");
		final Friend ls = new Friend("ls");
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				while(true)
					zs.bow(ls);
			}
		}).start();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				while(true)
					ls.bow(zs);
			}
		}).start();
		
	}
	
	static class Friend {
		private String name;
		
		public Friend(String name) {
			this.name = name;
		}
		
		public synchronized void bow(Friend friend) {
			System.out.println(Thread.currentThread().getName()+":::"+this.name+" bow to " + friend.name);
			friend.bowBack(this);//同步中调用其它对象的同步方法可能会发生死锁:相互等待对方释放锁
			System.out.println(Thread.currentThread().getName()+":::"+"bow done!");
		}
		
		public synchronized void bowBack(Friend friend) {
			System.out.println(Thread.currentThread().getName()+":::"+this.name+" bow to " + friend.name);
		}
	}
}

 

引起死锁的原因:需要相互获取对方锁的线程同时占有了自己的锁,导致对方无法获取到锁。

解决办法:通过设置同一个锁实现排他性访问,不给线程同时占有锁的机会。

 

package org.thread;
public class AvoidDeadLock {
	
	public static void main(String[] args) {
		final Friend zs = new Friend("zs");
		final Friend ls = new Friend("ls");
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				while(true)
					zs.bowEachOther(ls);
			}
		}).start();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				while(true)
					ls.bowEachOther(zs);
			}
		}).start();
		
	}
	
	static class Friend {
		private String name;
		
		public Friend(String name) {
			this.name = name;
		}
		
		public synchronized void bow(Friend friend) {
			System.out.println(Thread.currentThread().getName()+":::"+this.name+" bow to " + friend.name);
			friend.bowBack(this);//同步中调用其它对象的同步方法可能会发生死锁:相互等待对方释放锁
			System.out.println(Thread.currentThread().getName()+":::"+"bow done!");
		}
		
		public synchronized void bowBack(Friend friend) {
			System.out.println(Thread.currentThread().getName()+":::"+this.name+" bow to " + friend.name);
		}
		//额外引入一个能让线程排他性访问的方法,这些线程需要争夺同一个锁
		public void bowEachOther(Friend friend) {
			//使用同一个锁,实现排他性的访问
			synchronized (Friend.class) {
				bow(friend);
			}
		}
	}
}

 

B.线程饥饿

当某个线程占有了锁,并且需要执行很长一段时间才释放锁,这就导致其它等待锁释放的线程处于阻塞状态。这种情况下其它线程便处于“饥饿”状态。

 

C.线程活锁

活锁指的是线程间相互响应时,由于响应结果不正确而导致彼此一直都处于响应状态。

线程并没有阻塞,只是一直在响应而无法恢复到正常的工作中。

 

 

线程协作

实际开发中,很多场景都可以归结为生产者-消费者的协作关系。

基本规则:

任务池满的时候,阻塞生产者,直到任务池中有任务被取走;

任务池空的时候,阻塞消费者,直到任务池增加了新的任务;

实现原理:

1. 同步:生产者与消费者使用同一个锁

2. 协作:

wait()  线程判断条件不满足时,等待

notify()/notifyAll()  解除对方的等待

 

以下示例通过synchronized、while->wait()、notifyAll()模拟生产者-消费者的模型。

实际开发中,不需要自己再去发明轮子了,请使用java.util.concurrent包中的工具类完成需要的功能

 

package org.thread;
import java.util.concurrent.ThreadLocalRandom;

public class ConsumerProduerDemo {
	public static void main(String[] args) {
		Messenger messenger = new Messenger();
		new Consumer(messenger).start();
		new Producer(messenger).start();
	}

}

// ---共享资源:message
class Messenger {
	private String message;
	boolean empty = true;

	public synchronized String take() {
		while (empty) {
			try {
				System.out.println("Consumer waiting...");
				wait();
			} catch (InterruptedException e) {
			}
		}
		empty = true;
		notifyAll();
		return message;
	}

	public synchronized void put(String message) {
		while (!empty) {
			try {
				System.out.println("Producer waiting...");
				wait();
			} catch (InterruptedException e) {}
		}
		this.message = message;
		empty = false;
		notifyAll();
	}
}

// ---生产者
class Producer implements Runnable {

	private Messenger messenger;

	public Producer(Messenger messenger) {
		this.messenger = messenger;
	}

	@Override
	public void run() {
		String[] msgs = { "First msg", "Second msg", "Third msg", "Fouth msg" };
		for (int i = 0; i < msgs.length; i++) {
			messenger.put(msgs[i]);
			try {
				Thread.sleep(ThreadLocalRandom.current().nextInt(1000));
			} catch (InterruptedException e) {
			}
		}
		messenger.put("Done");
	}

	public Thread start() {
		Thread t = new Thread(this);
		t.start();
		return t;
	}
}

// ---消费者
class Consumer implements Runnable {

	private Messenger messenger;

	public Consumer(Messenger messenger) {
		this.messenger = messenger;
	}

	@Override
	public void run() {
		for (String msg = messenger.take(); !"Done".equals(msg); msg = messenger.take()) {
			System.out.println("Message take: " + msg);
			try {
				Thread.sleep(ThreadLocalRandom.current().nextInt(1000));
			} catch (InterruptedException e) {
			}
		}
	}

	public Thread start() {
		Thread t = new Thread(this);
		t.start();
		return t;
	}
}

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值