线程安全

今天针对线程安全,引入了synchronized的学习。复习了同步锁的使用方法,和使用特性。理解了在Java中如何处理同步问题。并使用生产者消费者模式进行简单的实现线程安全的问题

线程安全

多线程的执行顺序不可重现,但是必须要求执行结果必须可以重现。
线程的共享数据操作不完整性就一定会出现数据被破坏,而导致结果无法预知的问题—线程的安全问题。

同步处理的引入

在java语言中存在两种内建的synchronized语法:synchronized代码块和synchronized方法( 静态方法和非静态方法)可以解决线程安全问题。
首先synchronized将并行改为串行,当然会影响程序的执行效率,执行速度会受到影响。其次synchronized操作线程的堵塞,也就是由操作系统控制CPU的内核进行上下文的切换,这个切换本身也是耗时的。所以使用synchronized关键字会降低程序的运行效率。
问题
线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据
出错的解决方案
当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个名称叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁

  • 在Java中每个对象都有一个对象锁,其实就是对象头上的一个标记值而已
  • 同步处理的目标实际上就是实现线程排队执行的目的

写法1:同步方法

针对临界资源的修改方法上添加同步约束—synchronized。

//临界资源---被多个线程所共享操作的对象
public class OperNum {
	private int target;// 操作的数据
	// 针对操作数据的方法
	public synchronized void add() {// synchronized方法的同步锁为当前对象
		target++;
		System.out.println(Thread.currentThread().getName() + "add...." + target);
	}
	public synchronized void sub() {
		target--;
		System.out.println(Thread.currentThread().getName() + "sub...." + target);
	}
}

原理: 当一个线程在执行add方法时,其它线程不能执行add或者sub方法【同步方法都不能执行,因为是synchronized关键字会引入一个互斥锁,只有拥有锁的线程才能执行同步方法,其它线程只能阻塞等待】,synchronized属于重入锁,即当前线程可以执行其它的synchronized方法,但是其它线程不能执行当前对象中的synchronized方法,可以执行没有synchronized约束的方法 。

写法2:同步代码块

锁实际上是人为指定的,推荐使用synchronized(锁){}。

public class MyThread extends Thread {
	private static int target = 0; // 操作目标,临界资源
	private boolean flag = true; // 不是临界资源
	// 定义一个互斥锁
	private static Object lock = new Object();
	public MyThread(boolean flag) {
		this.flag = flag;
	}
	public void run() {
		for (int i = 0; i < 50; i++) {
			// synchronized (lock) 可以保证{}中的代码执行具备原子性
			synchronized (lock) {
				if (flag)
					target++;
				else
					target--;
				System.out.println(Thread.currentThread().getName() + (flag ? "add" : "sub") + "...." + target);
			}
		}
	}
	public static void main(String[] args) {
		for (int i = 0; i < 4; i++) {
			new MyThread(i % 2 == 0).start();
		}
	}
}

写法3:同步静态方法

以当前类对象充当锁,所有静态方法互斥

  • 和非静态方法不互斥,因为非静态同步方法的锁是当前对象,不是类锁
//临界资源
public class OperNum {
	private static int target = 10;// 操作的数据
	// 针对操作数据的方法
	public synchronized static void add() {
		target++;
		System.out.println(Thread.currentThread().getName() + "add...." + target);
	}
	public synchronized static void sub() {
		target--;
		System.out.println(Thread.currentThread().getName() + "sub...." + target);
	}
}

理解
在方法上添加synchronized关键字后就可以保证在一个时刻上只有一个线程在调用某个方法【锁只能有一个】,不会出现并发的情形,达到排队执行的效果。

  • 在Java中synchronized可保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代volatile功能),这点确实也是很重要的。
  • 常见的4种Java线程锁:原子量AtomicInteger、信号量Semaphone、同步处理synchronized和重入锁ReentrantLock。
  • jdk6之前是重量级锁,JDK6开始优化为锁的状态总共有四种,无锁状态(没有synchronized)、偏向锁、轻量级锁和重量级锁。锁状态的改变是根据竞争激烈程度进行的,在几乎无竞争的条件下,会使用偏向锁,在轻度竞争的条件下,会由偏向锁升级为轻量级锁, 在重度竞争的情况下,会升级到重量级锁。 随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。

讨论问题

public class A1 {
	public synchronized void aaa() throws InterruptedException {
		System.out.println("aaa...begin");
		Thread.sleep(100); // try-catch
		System.out.println("aaa...end");
	}

	public void bbb() {
		System.out.println("bbb...begin");
		System.out.println("bbb...end");
	}
}
  • 两个线程分别调用同一个对象中的aaa方法【方法中有sleep】和bbb方法,一个同步一个不同步,会不会有等待问题。临界资源对象中的同步方法和非同步方法不互斥,所以可以同时运行。
  • 两个方法都有synchronized则有等待。临界资源对象中的同步方法是互斥的,所以只能获取锁的线程运行,另外一个处于阻塞等待。
  • 一个方法静态,一个方法非静态。static方法使用的是类锁,而非静态方法使用的是对象锁、
  • 两个方法都是静态,因为类只有一个。如果两个方法都是静态的,则使用的都是类锁,所以会有等待方法中采用wait。
  • wait方法会有释放锁,而sleep不释放锁。

银行取钱操作

  • 定义账户类—临界资源
public class 账户类 {
	private Long id;
	private Double balance;
	public 账户类(long l, double d) {
		this.id=l;
		this.balance=d;
	}
	public Long getId() {
		return id;
	}
	public void setId(Long id) {
		this.id = id;
	}
	public Double getBalance() {
		return balance;
	}
	public void setBalance(Double balance) {
		this.balance = balance;
	}	
}
  • 定义取钱线程
public class 取钱线程 extends Thread {
	private 账户类 account;//取钱的账户
	private double amount;//取钱的金额
	public 取钱线程(账户类 account,double amount) {
		this.account=account;
		this.amount=amount;
	}
	@Override
	public void run() {
		synchronized (account) {
			//首先判断余额是否满足
			if(account.getBalance()>amount) {
				//阻塞200ms,模拟取钱过程
				try {
					Thread.sleep(200); //不会释放锁
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				//调用账户类的方法实现取钱处理
				account.setBalance(account.getBalance()-amount);
			}
		}
	}
}
  • 存钱线程,用于模拟银行发工资。上账时间1000ms;
public class 存钱线程 implements Runnable {
	private 账户类 account;
	private double amount;
	public 存钱线程(账户类 account,double salary) {
		this.account=account;
		this.amount=salary;
	}
	@Override
	public void run() {
		synchronized (account) {
		double dd=account.getBalance();
        try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
        dd+=amount;
        account.setBalance(dd);
		}
	}
}
  • 加粗样式
public class 测试类 {
	public static void main(String[] args)throws Exception {
		账户类 account = new 账户类(1L, 10000.);
		取钱线程 t1 = new 取钱线程(account, 9000);
		Thread t2 = new Thread(new 存钱线程(account, 500.));
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(account.getBalance());
	}
}

显示结果为10500.0,执行结果错误?

  • 共享同一个账户
  • 执行修改操作,但是没有进行同步约束
    如果解决出现的错误:使用同步处理—synchronized—加锁
    最简单的方法:
    使用方法2的同步代码块,在修改操作上添加对应的同步处理
public void run() {
	synchronized (account) { // 同步上下文
		// 首先判断余额是否满足
		if (account.getBalance() > amount) {
			// 阻塞200ms,模拟取钱过程
			try {
				Thread.sleep(200); // 不会释放锁
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			// 调用账户类的方法实现取钱处理
			account.setBalance(account.getBalance() - amount);
		}
	}
}

最常见的比较好想的方法:

//使用方法1的同步方法
public class 账户类 {
	private Long id;
	private Double balance;
	public 账户类(long l, double d) {
		this.id = l;
		this.balance = d;
	}
	public synchronized void 存钱(double salary) {
		double dd = balance;
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		dd += salary;
		this.balance = dd;
	}
	public synchronized void 取钱(double amount) {
		// 首先判断余额是否满足
		if (balance > amount) {
			// 阻塞200ms,模拟取钱过程
			try {
				Thread.sleep(200); // 不会释放锁
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			// 调用账户类的方法实现取钱处理
			this.balance = this.balance - amount;
		}
	}
	public Double getBalance() {
		return this.balance;
	}
}

小结synchronized

synchronized的三种应用:对象锁、类锁和同步块
注意synchronized不能修饰构造器、变量、内部类等

同步实例方法
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。
方法上添加synchronized就叫做同步方法,例如public synchronized void draw(double amount){}。
注意:在一个类对象中所有的同步方法是互斥的。

  • 只要有一个线程进行了当前类对象(只能一个对象)的同步方法,则不允许其它线程在进入当前这个对象的任何同步方法,但是允许进入非同步方法
  • 同样当前线程则可以进入当前类对象的其它同步方法,也允许进入非同步方法,当线程进入同步方法,则获取同步锁,离开同步方法则释放同步锁
  • 这个锁就是当前类对象

这种方法不是最佳选择,因为这里的同步处理颗粒度太大了(所有当前对象中的同步处理方法都是互斥的),会影响并发性。

线程安全的类:是通过使用同步方法的类,同步监视器是this

  • 该类的对象可以被多个线程安全地访问
  • 每个线程调用该对象的任意方法之后都将得到正确结果
  • 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态

同步静态方法
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。
同步监视器是当前类对象Class c=Date.class。

public class App {
	private static int num = 0;

	public static synchronized void add() {
		System.out.println(Thread.currentThread().getName() + ":begin...." + new Date());
		int cc = num;
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		cc++;
		num = cc;
		System.out.println(cc);
		System.out.println(Thread.currentThread().getName() + ":end...." + new Date());
	}
	public static void main(String[] args) throws Exception {
		Thread[] ts = new Thread[10];
		for (int i = 0; i < ts.length; i++) {
			Runnable r = new Runnable() {
				public void run() {
					App app = new App();
					App.add();
				}
			};
			ts[i] = new Thread(r);
			ts[i].start();
		}
		for (Thread temp : ts) {
			temp.join();
		}
		System.out.println("Main:" + num);
	}
}

注意:如果在方法上去掉static,则不能实现锁定效果,因为是对象锁,而在线程中创建了10个对象,各个对象没有任何关系,所以不能达到锁定的效果,但是类锁的效果仍旧有效
同步方法块
上面的两种方式比较死板,普通方法同步是以当前对象作为锁,静态方法同步是以当前类对象作为锁,所以引入更为灵活的方式:同步块,将锁对象作为参数进行传递synchronized(account){}同步监视器可以阻止多个线程对同一个共享资源的并发访问,任何时刻只有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放堆该同步监视器的锁定。
使用synchronized加锁的字节码会出现monitorenter和monitorexit两个指令,可以理解为代码块执行
前的加锁和退出同步时的解锁

  • 执行monitorenter指令时,线程会为锁对象关联一个ObjectMonitor对象。
  • 线程遇到synchronized同步时,先会进入ObjectMonitor对象的EntryList队列中,然后尝试把ObjectMonitor对象的owner变量设置为当前线程,同时ObjectMonitor对象的monitor中的计数器count加1,即获得对象锁。否则通过尝试自旋一定次数加锁,失败则进入ObjectMonitor对象的cxq队列阻塞等待
  • synchronized是可重入,非公平锁,因为entryList的线程会先自旋尝试加锁,而不是加入cxq排队等待,不公平

何时需要同步
在多个线程同时访问互斥(可交换)数据时,应该同步以保护数据,确保两个线程不会同时修改更改它。
对于非静态字段中可更改的数据,通常使用非静态方法访问。
对于静态字段中可更改的数据,通常使用静态方法访问。
释放同步锁

  • 同步代码执行结束
  • 同步方法中遇到break或return终止代码

多线程之间的数据通讯方法

生产者消费者模式
生产/消费者问题是个非常典型的多线程问题,涉及到的对象包括生产者、消费者、仓库和产品。他们之间的关系如下:

  • 生产者仅仅在仓储未满时候生产,仓满则停止生产。
  • 消费者仅仅在仓储有产品时候才能消费,仓空则等待。
  • 当消费者发现仓库没产品可消费时候会通知生产者生产。
  • 生产者在生产出可消费产品时候,应该通知等待的消费者去消费。

为什么要使用生产者/消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这种生产消费能力不均衡的问题,所以便有了生产者和消费者模式。
生产者/消费者模型优点
1、解耦。因为多了一个缓冲区,所以生产者和消费者并不直接相互调用,这一点很容易想到,这样生产者和消费者的代码发生变化,都不会对对方产生影响,这样其实就把生产者和消费者之间的强耦合解开,变为了生产者和缓冲区/消费者和缓冲区之间的弱耦合.
2、通过平衡生产者和消费者的处理能力来提高整体处理数据的速度,这是生产者/消费者模型最重要的一个优点。如果消费者直接从生产者这里拿数据,如果生产者生产的速度很慢,但消费者消费的速度很快,那消费者就得占用CPU的时间片白白等在那边。有了生产者/消费者模型,生产者和消费者就是两个独立的并发体,生产者把生产出来的数据往缓冲区一丢就好了,不必管消费者;消费者也是,从缓冲区去拿数据就好了,也不必管生产者,缓冲区满了就不生产,缓冲区空了就不消费,使生产者/消费者的处理能力达到一个动态的平衡。
生产者/消费者模式的作用…==

  • 支持并发。
  • 解耦
  • 支持忙闲不均

调用wait/notify之类的方法要求必须在当前线程对象内部,例如synchronized方法中

编码实现

仓库类,产品就是仓库的属性data:

//临界资源
public class Basket {
	private volatile Object data;
	//生产者向仓库中存放数据
	public synchronized void product(Object data) {
		//如果仓库中有数据,则生产者进入阻塞等待,直到其它线程唤醒
		while(this.data!=null) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		//如果没有数据则进行生产操作
		this.data=data;
		System.out.println(Thread.currentThread().getName()+"生产了一个日期"+this.data);
		this.notifyAll();//唤醒在当前对象上处于wait的所有线程
	}
	//消费者从仓库中消费数据
	public synchronized void consume() {
		//如果仓库中没有数据,则消费者进入阻塞等待,直到其它线程唤醒
		while(this.data==null)
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		//如果有数据data!=null,则执行消费操作
		System.out.println(Thread.currentThread().getName()+"消费了一个数据"+this.data);
		this.data=null;
		this.notifyAll();  //唤醒在当前对象上处于wait的所有线程
	}
}

生产者线程负责生产产品,并和消费者共享仓库

public class Producer extends Thread {
	private Basket basket;
	//通过构造器传入对应的basket对象
	public Producer(Basket basket) {
		this.basket=basket;
	}
	@Override
	public void run() {
		//生产20次日期对象
		for(int i=0;i<20;i++) {
			Object data=new Date();  //生产者生产的具体产品
			basket.product(data);
		}
	}
}

消费者线程负责消费产品,并和生产者共享仓库

public class Consumer extends Thread {
	private Basket basket;

	public Consumer(Basket basket) {
		this.basket = basket;
	}

	@Override
	public void run() {
		// 消费20次日期对象
		for (int i = 0; i < 20; i++) {
			basket.consume();
		}
	}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值