2.并发同步

一.线程不安全问题

  1. 结论:在线程的run方法上不能使用throws来声明抛出异常,只能在方法中使用try-catch来处理异常
  2. 原因:子类覆盖父类方法的原则,子类不能抛出新的异常.
  3. 在Runnable接口中的run方法,没有抛出异常.

image

  1. 解决方案:
    保证打印苹果和苹果总数减1操作必须同步完成.
    A线程进入操作的时,B和C线程只能在外面等着,A操作结束,A或B或C才有机会进入代码去执行

二.同步代码块

语法:
synchronized(同步锁){
需要同步操作的代码
}
  1. 为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制.
  2. 同步监听对象/同步锁/同步监听器/互斥锁:
    对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
  3. Java程序运行使用任何对象作为同步监听对象,但是一般的,我们使用当前并发访问的共同资源作为同
    步监听对象.
    注意:在任何时候,最多允许一个线程拥有同步锁

image

三.同步方法

  1. 同步方法:使用synchronized修饰的方法,就叫同步方法,保证A线程执行该方法的时候,其他线程只能
    在方法外等着.
synchronized public doWork(){

}
  1. 同步锁:
    对于非static方法,同步锁就是this.
    对于static方法,我们使用当前方法所在类的字节码对象(Apple2.class)

  2. 不要使用synchonized修饰run方法,修饰之后,某一个线程就执行完了所有的功能.,好比是多个线程
    出现串行.

  3. 解决方案:把需要同步的操作的代码定义在一个新的方法中,并且该方法使用synchronized修饰,
    再在run方法中调用该新的方法即可.

  4. synchronized的好与坏:

    1. 好处:保证了多线程并发访问时的同步操作,避免线程的安全性问题.
    2. 缺点:使用synchronized的方法/代码块的性能比不用要低一些
    3. 建议:尽量减小synchronized的作用域.
  5. 面试题:

    1. StringBuilder和StringBuffer的区别
    2. 说说ArrayList和Vector的区别
    3. HashMap和Hashtable的区别
      后者的方法使用synchronized修饰,保证线程安全,前者都没有使用,性能更高.

image

四.单例模式 - 懒加载

饿汉式

//单例模式-饿汉式
public class ArrayUtil {
	private static ArrayUtil instance = new ArrayUtil();
	private ArrayUtil() {}
	public static ArrayUtil getInstance() {
		return instance;
	}
	public void sort(int[] arr) {}
}

懒汉式,存在线程安全问题.
可能创建多个线程

//懒汉式
public class ArrayUtilLazy {
	private static ArrayUtilLazy instane_lazy = null;
	private ArrayUtilLazy() {}
	public static ArrayUtilLazy getinstace() {
		if(instane_lazy != null) {
			instane_lazy = new ArrayUtilLazy(); 
		}
		return instane_lazy;
	}
	public void lazyUtil() {}
}

使用snychronized修饰
//懒汉式
public class ArrayUtilLazy {
	private static ArrayUtilLazy instane_lazy = null;
	private ArrayUtilLazy() {}
	//同步方法:此时的同步监听对象式(ArrayUtilLazy.class)
	synchronized public static ArrayUtilLazy getinstace() {
		if(instane_lazy != null) {
			instane_lazy = new ArrayUtilLazy(); 
		}
		return instane_lazy;
	}
	public void lazyUtil() {}
}

上述已经解决懒汉式线程安全问题,
但是synchronized的作用域太大了,损耗性能----.>尽量减小synchronized的作用域.
解决方案:使用双重检查锁机制.

  1. 双重检查加锁:
    1. 作用:既实现线程安全,又能够使性能不受很大影响,
    2. 含义:并不是每次进入getInstace方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,
      如果不存在才进行下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果
      不存在,就在同步的情况下创建一个实例,这是第二重检查.这样一来,就只需要同步一次了,从而
      减少了多次在同步情况下进行判断所浪费的时间.
//懒汉式
public class ArrayUtilLazy {
	private static ArrayUtilLazy instane_lazy = null;
	private ArrayUtilLazy() {}
	//同步方法:此时的同步监听对象式(ArrayUtilLazy.class)
public static ArrayUtilLazy getinstace() {
		if(instane_lazy != null) {
			synchronized (ArrayUtilLazy.class) {
				if(instane_lazy != null) {
				instane_lazy = new ArrayUtilLazy(); 
				}				
			}
		}
		return instane_lazy;
	}
	public void lazyUtil() {}
}

“双重检查加锁”:机制的实现会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地
线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量.

//懒汉式
public class ArrayUtilLazy {
	private static volatile ArrayUtilLazy instane_lazy = null;
	private ArrayUtilLazy() {}
	//同步方法:此时的同步监听对象式(ArrayUtilLazy.class)
public static ArrayUtilLazy getinstace() {
		if(instane_lazy != null) {
			synchronized (ArrayUtilLazy.class) {
				if(instane_lazy != null) {
				instane_lazy = new ArrayUtilLazy(); 
				}				
			}
		}
		return instane_lazy;
	}
	public void lazyUtil() {}
}

注意:在java1.4以前版本中,很多JVM对于volatile关键字的实现的问题,会导致"双重检查加锁"的失败,
所以只能用在Java1.5及以上版本.
提示:由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高.没有特
别的需要,不要使用,虽然可以使用"双重检查加锁"机制实现线程安全的单例,但不建议大量采用.
建议使用饿汉式即可.安全,简单

五.Lock

比Synchronized更强大
新建LockDemo.java

class Apple2 implements Runnable {
	ReentrantLock rk = new ReentrantLock();//创建一个锁对象
	private int count = 50;//苹果总数

	@Override
	public void run() {
		for (int i = 0; i < 50; i++)  {
			eat();
		}
	}

	private void eat() {
		rk.lock();//锁获取锁
		try {
			if (count > 0) {
				System.out.println(Thread.currentThread().getName() + "  吃了编号为:" + count + "的苹果");
				--count;
				Thread.sleep(10);
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			rk.unlock();//释放锁
		}
	}
}

public class LockDemo {
	public static void main(String[] args) {
		Apple2 a = new Apple2();
		new Thread(a, "小A").start();
		new Thread(a, "小B").start();
		new Thread(a, "小C").start();
	}
}

六.线程通信

  1. 不同的线程执行不同的任务,如果这些任务有某些关系,线程之间必须能够通信,协调完成工作,
    经典的产生者和消费者.案例(Producer/Consumer)
    分析案例:
    1. 生产者和消费者应该操作共享的资源(实现方式来做)
    2. 使用一个或多个线程来表示生产者(Producer)
    3. 使用一个或多个线程表示消费者(Consumer)

新建SharingResource.java

//共享资源对象(姓名-性别)
public class SharingResource {
	private  String name ;
	private  String gender;
	
	/**
	 * 生产者想共享资源对象中存储数据
	 * @param name		存储的姓名
	 * @param gender		存储的性别
	 */
	public void push(String name,String gender) {
		this.name = name;
		this.gender = gender;
	}
	/**
	 * 消费者从共享资源对象中取出数据
	 */
	public void popup() {
		System.out.println(name + "-" + gender);
	}
}

新建Producter.java

//生产者
public class Producter implements Runnable{
	SharingResource sr = null;
	public Producter(SharingResource sr) {
		this.sr = sr;
	}
	@Override
	public void run() {
		for (int i = 0; i < 50; i++) {
			if (i % 2 == 0) {
				sr.push("春哥哥","男" );				
			}else
				sr.push("凤姐","女");
		}
	}
}

新建Consumer.java

//消费者
public class Consumer implements Runnable{
	private SharingResource sr = null;
	public Consumer(SharingResource sr){
		this.sr = sr;
	}
	@Override
	public void run() {
		for (int i = 0; i < 50; i++) {
			sr.popup();
		}
	}
}

新建App.java

//测试类
public class App {
	public static void main(String[] args) {
		//创建共享的资源文件
		SharingResource sr = new SharingResource();
		//创建生产者对象
		Producter p = new Producter(sr);
		//创建消费者对象
		Consumer c = new Consumer(sr);
		new Thread(p).start();
		new Thread(c).start();
	}
}

运行结果都为 凤姐-女

七.线程通信-解决性别紊乱问题

  1. 分析生产者和消费者案例存在的问题
    建议在生产者和性别之间以及在打印之间使用Thread.sleep(10);
    使效果更明显.
    出现下面的情况:

image

  1. 解决方案:只要保证在生产姓名和性别的过程保持同步,中间不能被消费者线程进来取走数据.
    可以使用同步代码块/同步方法/Lock机制来保持同步性

修改SharingResource.java

//共享资源对象(姓名-性别)
public class SharingResource {
	private String name;
	private String gender;

	/**
	 * 生产者想共享资源对象中存储数据
	 * @param name		存储的姓名
	 * @param gender		存储的性别
	 */
	synchronized public void push(String name, String gender) {
		this.name = name;
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.gender = gender;
	}

	/**
	 * 消费者从共享资源对象中取出数据
	 */
	synchronized public void popup() {
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(name + "-" + gender);
	}
}

八.解决重复消费的问题

![image][repeat]

  1. 解决方案:得使用等待唤醒机制

  2. 同步锁池:
    同步锁必须选择多个线程共同的资源对象.
    当前生产者在生产数据的时候(先拥有同步锁),其他线程就在锁池中等待获取锁.
    当执行完同步代码块或同步方法时,就是释放同步锁,其他线程开始抢锁的使用权.

  3. 线程通信-wait和notify方法介绍
    java.lang.Object类提供两类用于操作线程通信的方法.

  4. wait():执行该方法的线程对象释放同步锁,JVM把该线程存放到等待池中,等待其他的线程唤醒该线程.

  5. notify():执行该方法的线程唤醒在等待池中等待的任意一个线程,把线程转到锁池中等待.
    notifyAll():执行该方法的线程唤醒在等待池中等待的所有的线程,把线程转到锁池中等待.

注意:上诉方法只能被同步监听锁对象来调用,否则报错,IllegalMonitorStateException

  1. 多个线程只有使用相同的一个对象的时候,多线程之间才有互斥效果,
    我们把这个用来做互斥的对象称之为,同步监听对象/同步锁

  2. 同步锁对象可以是任意类型对象,只需保证多个线程使用的是相同的锁对象即可.
    因为只有同步锁对象才能调用wait和notily方法,所以wait和notily方法应该在Object类中

  3. 假设A线程和B线程共同操作一个X对象(同步锁),A,B线程可以通过X对象的wait和notify方法来进行通信,

    1. 当A线程执行X对象的同步方法时,A线程持有X对象的锁,B线程没有执行机会,B线程在X对象的锁池
      中等待
    2. A线程在同步方法中执行X.wait()方法时,A线程释放X对象的锁,A线程进入x对象的等待池中.
    3. 在x对象的锁池中等待的B线程获取x对象的锁,执行x的另一个同步方法
    4. B线程在同步方法中执行,x.notify()方法时,JVM把A线程从x对象的等待池中移动到x对象的锁池中,
      等待获取锁
    5. B线程执行完同步方法,释放锁,A线程获得锁,进行执行同步方法.

修改SharingResource.java

//共享资源对象(姓名-性别)
public class SharingResource {
	private String name;
	private String gender;
	//表示共享资源对象是否为空的状态
	private boolean isNull = true;

	/**
	 * 生产者想共享资源对象中存储数据
	 * @param name		存储的姓名
	 * @param gender		存储的性别
	 */
	synchronized public void push(String name, String gender) {
		try {
			//当前isNull为false时,不空等着消费者来获取
			while (!isNull) {
				//使用同步锁对象来调用,表示当前线程释放同步锁,进入等待池,只能被其他线程唤醒
				this.wait();
			}
			//------开始生产-------
			this.name = name;
			Thread.sleep(10);
			this.gender = gender;
			//------生产结束---------
			isNull = false;//设置共享资源中的数据不为空
			this.notify();//唤醒一个消费者
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	/**
	 * 消费者从共享资源对象中取出数据
	 */
	synchronized public void popup() {
		try {
			//当前isNull为true时让生产者来生产
			while (isNull) {
				//使用同步锁对象来调用,表示当前线程释放同步锁,进入等待池,只能被其他线程唤醒
				this.wait();
			}
			//---------开始消费--------
			Thread.sleep(10);
			System.out.println(name + "-" + gender);
			//-------消费结束--------
			isNull = true;//表示数据为空,让生产者来生产
			this.notify();//唤醒生产者
		} catch (InterruptedException e1) {
			e1.printStackTrace();
		}
	}
}

九.线程通信 Lock和Condition接口

wait和notify方法,只能被同步监听锁对象来调用,否则报错IllegalMonitorStateException
Lock机制根本没有同步锁,没有自动获取锁和自动释放锁的概念
Lock机制不能调用wait和notify方法
解决方案:Java5中提供了Lock机制的同时提供了处理Lock机制的通信控制的Condition接口

修改SharingResource.java

public class SharingResource {
	private String name;
	private String gender;
	private boolean isNull = true;
	private final Lock lock = new ReentrantLock();
	// 返回绑定到此 Lock 实例的新 Condition 实例。
	private Condition condition = lock.newCondition();
	/**
	 * 
	 * @param name
	 * @param gender
	 */
	public void push(String name,String gender) {
		lock.lock();
		try {
			while(!isNull) {
				// 造成当前线程在接到信号或被中断之前一直处于等待状态。
				condition.await();
			}
			this.name = name;
			Thread.sleep(10);
			this.gender = gender;
			isNull = false;
			//唤醒一个等待线程。
			condition.signal();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}finally {
			lock.unlock();
		}
	}
	/**
	 * 取出数据
	 */
	public void popup() {
		lock.lock();
		try {
			while(isNull) {
				condition.await();
			}
			Thread.sleep(10);
			System.out.println(name + gender);	
			isNull = true;
			condition.signal();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}finally {
			lock.unlock();
		}
	}
}

十.死锁

  1. 多线程通信的时候很容易造成死锁,死锁无法解决,只能避免.

  2. 当A线程等待由B线程持有的锁,B线程正在等待A线程持有的锁时,发生死锁想象,JVM不检查也不试图避
    免这种情况,所以程序员必须保证不导致死锁.
    避免死锁法则:当多个线程都要访问共享的资源A,B,C时,保证每一个线程都按照相同的顺序去访问它们,
    比如先A,后B,最后C

  3. Thred类中过时的方法:

    1. suspend():使正在运行的线程放弃CPU,暂停运行.
    2. resume():使暂停的线程恢复运行

注意:因为容易导致死锁,所以已经被废弃了,
4. 死锁情况:

A线程获得对象锁,正在执行一个同步方法,如果B线程调用A线程的suspend方法,此时A线程暂停运行,
此时A线程放弃CPU,但是不会放弃占用的锁.

发布了58 篇原创文章 · 获赞 0 · 访问量 709

猜你喜欢

转载自blog.csdn.net/huang_kuh/article/details/105251705
2.