文章目录
一.线程不安全问题
- 结论:在线程的run方法上不能使用throws来声明抛出异常,只能在方法中使用try-catch来处理异常
- 原因:子类覆盖父类方法的原则,子类不能抛出新的异常.
- 在Runnable接口中的run方法,没有抛出异常.
- 解决方案:
保证打印苹果和苹果总数减1操作必须同步完成.
A线程进入操作的时,B和C线程只能在外面等着,A操作结束,A或B或C才有机会进入代码去执行
二.同步代码块
语法:
synchronized(同步锁){
需要同步操作的代码
}
- 为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制.
- 同步监听对象/同步锁/同步监听器/互斥锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁. - Java程序运行使用任何对象作为同步监听对象,但是一般的,我们使用当前并发访问的共同资源作为同
步监听对象.
注意:在任何时候,最多允许一个线程拥有同步锁
三.同步方法
- 同步方法:使用synchronized修饰的方法,就叫同步方法,保证A线程执行该方法的时候,其他线程只能
在方法外等着.
synchronized public doWork(){
}
-
同步锁:
对于非static方法,同步锁就是this.
对于static方法,我们使用当前方法所在类的字节码对象(Apple2.class) -
不要使用synchonized修饰run方法,修饰之后,某一个线程就执行完了所有的功能.,好比是多个线程
出现串行. -
解决方案:把需要同步的操作的代码定义在一个新的方法中,并且该方法使用synchronized修饰,
再在run方法中调用该新的方法即可. -
synchronized的好与坏:
- 好处:保证了多线程并发访问时的同步操作,避免线程的安全性问题.
- 缺点:使用synchronized的方法/代码块的性能比不用要低一些
- 建议:尽量减小synchronized的作用域.
-
面试题:
- StringBuilder和StringBuffer的区别
- 说说ArrayList和Vector的区别
- HashMap和Hashtable的区别
后者的方法使用synchronized修饰,保证线程安全,前者都没有使用,性能更高.
四.单例模式 - 懒加载
饿汉式
//单例模式-饿汉式
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的作用域.
解决方案:使用双重检查锁机制.
- 双重检查加锁:
- 作用:既实现线程安全,又能够使性能不受很大影响,
- 含义:并不是每次进入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();
}
}
六.线程通信
- 不同的线程执行不同的任务,如果这些任务有某些关系,线程之间必须能够通信,协调完成工作,
经典的产生者和消费者.案例(Producer/Consumer)
分析案例:- 生产者和消费者应该操作共享的资源(实现方式来做)
- 使用一个或多个线程来表示生产者(Producer)
- 使用一个或多个线程表示消费者(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();
}
}
运行结果都为 凤姐-女
七.线程通信-解决性别紊乱问题
- 分析生产者和消费者案例存在的问题
建议在生产者和性别之间以及在打印之间使用Thread.sleep(10);
使效果更明显.
出现下面的情况:
- 解决方案:只要保证在生产姓名和性别的过程保持同步,中间不能被消费者线程进来取走数据.
可以使用同步代码块/同步方法/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]
-
解决方案:得使用等待唤醒机制
-
同步锁池:
同步锁必须选择多个线程共同的资源对象.
当前生产者在生产数据的时候(先拥有同步锁),其他线程就在锁池中等待获取锁.
当执行完同步代码块或同步方法时,就是释放同步锁,其他线程开始抢锁的使用权. -
线程通信-wait和notify方法介绍
java.lang.Object类提供两类用于操作线程通信的方法. -
wait():执行该方法的线程对象释放同步锁,JVM把该线程存放到等待池中,等待其他的线程唤醒该线程.
-
notify():执行该方法的线程唤醒在等待池中等待的任意一个线程,把线程转到锁池中等待.
notifyAll():执行该方法的线程唤醒在等待池中等待的所有的线程,把线程转到锁池中等待.
注意:上诉方法只能被同步监听锁对象来调用,否则报错,IllegalMonitorStateException
-
多个线程只有使用相同的一个对象的时候,多线程之间才有互斥效果,
我们把这个用来做互斥的对象称之为,同步监听对象/同步锁 -
同步锁对象可以是任意类型对象,只需保证多个线程使用的是相同的锁对象即可.
因为只有同步锁对象才能调用wait和notily方法,所以wait和notily方法应该在Object类中 -
假设A线程和B线程共同操作一个X对象(同步锁),A,B线程可以通过X对象的wait和notify方法来进行通信,
- 当A线程执行X对象的同步方法时,A线程持有X对象的锁,B线程没有执行机会,B线程在X对象的锁池
中等待 - A线程在同步方法中执行X.wait()方法时,A线程释放X对象的锁,A线程进入x对象的等待池中.
- 在x对象的锁池中等待的B线程获取x对象的锁,执行x的另一个同步方法
- B线程在同步方法中执行,x.notify()方法时,JVM把A线程从x对象的等待池中移动到x对象的锁池中,
等待获取锁 - B线程执行完同步方法,释放锁,A线程获得锁,进行执行同步方法.
- 当A线程执行X对象的同步方法时,A线程持有X对象的锁,B线程没有执行机会,B线程在X对象的锁池
修改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();
}
}
}
十.死锁
-
多线程通信的时候很容易造成死锁,死锁无法解决,只能避免.
-
当A线程等待由B线程持有的锁,B线程正在等待A线程持有的锁时,发生死锁想象,JVM不检查也不试图避
免这种情况,所以程序员必须保证不导致死锁.
避免死锁法则:当多个线程都要访问共享的资源A,B,C时,保证每一个线程都按照相同的顺序去访问它们,
比如先A,后B,最后C -
Thred类中过时的方法:
- suspend():使正在运行的线程放弃CPU,暂停运行.
- resume():使暂停的线程恢复运行
注意:因为容易导致死锁,所以已经被废弃了,
4. 死锁情况:
A线程获得对象锁,正在执行一个同步方法,如果B线程调用A线程的suspend方法,此时A线程暂停运行,
此时A线程放弃CPU,但是不会放弃占用的锁.