前言
上一节中我们学习到了在使用JAVA 多线程时,如果多线程间存在着使用公用数据时,将会出现线程安全问题,那么到底什么是线程安全问题呢?
- 线程安全
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。百度百科 - 线程安全问题
在多线程中使用共享数据会出现共享数据不一致等问题,在遇到多线程的线程安全问题时,通常而言我们可以通关过引入同步机制来解决线程安全问题,但请注意只是通常而言。因为一些多线程中即使我引入了同步机制也很难解决线程安全问题。
概要
同步机制
在并发程序设计中,各进程对公共变量的访问必须加以制约,这种制约称为同步。在JAVA中有两种主要的同步方式:同步代码块(给代码块加同步锁)
给某一段代码(共享数据)添加一个同步锁,这个同步锁是一个对象。大体长这个样子:synchronized (object) { //你的逻辑 }
同步方法(给方法加同步锁)
给某一指定的方法添加一个同步锁(此时的同步锁默认为this:多线程中类的示例-有时不唯一),大体上是这个样子的:public synchronized void produce() { //你的逻辑 }
下面我们将通过两个实际的例子来学习多线程线程安全的实现。
基于继承Thread类的多线程的线程安全解决方法
例子
场景说明
某售票厅出售100张票,分三个窗口出售,用代码模拟用户购票信息。代码示例
package coreJavaReview.thread; /** * 使用继承Thread类实现多线程时的线程安全问题解决 >同步代码锁 >同步方法 * * @author Dustyone * */ public class ThreadPurchaseTikectThreadSecurity { public static void main(String[] args) { // 使用同步 PurchanseThreadSecurityBlock pb1 = new PurchanseThreadSecurityBlock(); PurchanseThreadSecurityBlock pb2 = new PurchanseThreadSecurityBlock(); PurchanseThreadSecurityBlock pb3 = new PurchanseThreadSecurityBlock(); pb1.setName("第一个窗口使用同步代码块"); pb2.setName("第二个窗口使用同步代码块"); pb3.setName("第三个窗口使用同步代码块"); //pb1.start(); pb2.start(); pb3.start(); PurchanseThreadSecurityMethod pm1 = new PurchanseThreadSecurityMethod(); PurchanseThreadSecurityMethod pm2 = new PurchanseThreadSecurityMethod(); PurchanseThreadSecurityMethod pm3 = new PurchanseThreadSecurityMethod(); pm1.setName("第一个窗口使用同步方法"); pm2.setName("第二个窗口使用同步方法"); pm3.setName("第三个窗口使用同步方法"); pm1.start(); pm2.start(); pm3.start(); } } /** * 同步代码块 * * @author Dustyone * */ class PurchanseThreadSecurityBlock extends Thread { static int i = 1; static Object object = "K"; public void run() { while (true) { // synchronized (this) { // //此时无法实现线程安全,因为通过继承Thread类实现多线程添加同步锁使用this时,this表示当前继承Thread的类对象(pm1、pm2、pm3)。是随着被调用的继承Thread的类对象变化而改变的。因为同步锁不唯一,进而无法确保多线程间的线程安全问题 synchronized (object) { // 如果使用一个静态的对象来作为当前多线程同步锁时,该静态对象是唯一的,能过实现同步锁唯一,进而确保进而无法确保多线程间的线程安全问题 if (i <= 100) { try { Thread.currentThread().sleep(100); } catch (Exception e) { } System.out.println(Thread.currentThread().getName() + "出售了第" + i + "张票"); i++; } } } } } /** * 使用同步方法实现线程安全 * * @author Dustyone * */ class PurchanseThreadSecurityMethod extends Thread { static int i = 1; public void run() { while (true) { produce(); } } public synchronized void produce() { //此时的同步锁依然为this(pm1,pm2、pm3),如果通过继承Thread实现多线程即使使用同步方法依旧无法解决线程安全问题 if (i <= 100) { try { Thread.currentThread().sleep(100); } catch (Exception e) { } System.out.println(Thread.currentThread().getName() + "出售了第" + i + "张票"); i++; } } }
如上示例可能会出现即使使用了同步锁仍会出现先线程安全问题。如
在使用同步代码块时,添加的同步锁为this时,线程安全问题依旧存在,因为this是继承Thread类的实例对象(pb1、pb2、pb3)不唯一,因而导致线程同步锁不唯一,进而无法确保线程安全。解决方法:使用唯一的同步锁(比如某个静态变量,某个类的实例-通过反射机制实现 等等)。
在使用同步方法时,默认添加的同步锁为this,因为this是继承Thread类的实例对象(pb1、pb2、pb3/pm1、pm2、pm3)不唯一,因而导致线程同步锁不唯一,进而无法确保线程安全。此时将无法解决
小结:基于继承Thread类的多线程要想解决线程安全问题以确保共享数据的一致性和完整性,很难通过使用同步代码块的方式来实现,基本无法通关过同步方法来实现。因为基于继承Thread类的多线程中,多线程的实例(this)实际上是不唯一的,y因为同步锁也不唯一
基于实现Runnable接口的多线程的线程安全解决方法
- 场景说明
某售票厅出售100张票,分三个窗口出售,用代码模拟用户购票信息。 代码示例
package coreJavaReview.thread; /** * 线程同步是解决线程安全的方法之一。 实现线程同步一般有两种方法 >同步代码块 * * >同步方法 * * @author Dustyone * */ public class ThreadPurchaseTikectRunnableSecurity { public static void main(String[] args) { // 同步代码快 PurchaseTikectRunnbaleWithSyncBlock pb1 = new PurchaseTikectRunnbaleWithSyncBlock(); Thread t1 = new Thread(pb1); Thread t2 = new Thread(pb1); Thread t3 = new Thread(pb1); t1.setName("第一个窗-使用同步代码锁"); t2.setName("第二个窗-使用同步代码锁"); t3.setName("第三个窗-使用同步代码锁"); /* * t1.start(); t2.start(); t3.start(); */ // 同步方法 PurchaseTikectRunnbaleWithSyncMethod pm1 = new PurchaseTikectRunnbaleWithSyncMethod(); Thread t4 = new Thread(pm1); Thread t5 = new Thread(pm1); Thread t6 = new Thread(pm1); t4.setName("第一个窗-使用同步方法"); t5.setName("第二个窗-使用同步方法"); t6.setName("第三个窗-使用同步方法"); t4.start(); t5.start(); t6.start(); } } /** * 使用同步代码块实现线程同步 * * @author Dustyone * */ class PurchaseTikectRunnbaleWithSyncBlock implements Runnable { static int i = 1; public void run() { while (true) { synchronized (this) { if (i <= 100) { try { Thread.currentThread().sleep(100); } catch (Exception e) { } System.out.println(Thread.currentThread().getName() + "出售了第" + i + "张票"); i++; } } } } } /** * 使用同步方法实现线程同步 * * @author Dustyone * */ class PurchaseTikectRunnbaleWithSyncMethod implements Runnable { static int i = 1; public void run() { while (true) { produce(); } } public synchronized void produce() { if (i <= 100) { try { Thread.currentThread().sleep(100); } catch (Exception e) { } System.out.println(Thread.currentThread().getName() + "出售了第" + i + "张票"); i++; } } }
- 场景说明
如上示例能够很好地实现多线程中的线程安全,应为Thread类的实例唯一(pb1/pm1),因为确保了同步锁的唯一性
小结
线程同步的实现有两种
- 同步代码块
- 同步方法
通过上面两个示例我们可以看出,线程安全问题能否避免其实无线程同步方法的选择没有必然联系,它直接有实现多线程的方法决定。
- 基于继承Thread类实现多线程
由于该方法无论使用哪一种线程同步方法都基本很难确保同步锁的唯一性(指定同步锁除外),无法确保线程安全 - 基于实现Runnable接口实现多线程(推荐使用)
该方法能确保同步锁的唯一性,能确保线程安全。