线程同步synchronized小结
1、线程同步的目的是为了保护多个线程访问一个资源时对资源的破坏。
- 多线程的执行过程不可重现,可能会造成对共享数据的破坏
- volatile可见性和有序性,但是不能保证原子性
- synchronized可以保证原子性
2、线程同步方法是通过锁(监视者)来实现,每个对象都有且仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的同步方法(可以访问静态同步方法)。
3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
4、对于同步,要时刻清醒在哪个对象上同步,这是关键
- 同步方法–当前对象
- 静态同步方法–当前类
- 同步代码块–任意指定
5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对需要具有原子操作的步骤做出分析,并保证原子操作期间别的线程无法访问竞争资源(加锁处理)
- StringBuilder和StringBuffer
- JDK6+开始对synchronized进行优化,引入了偏向锁、轻量级锁和重量级锁
6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞
- wait方法和sleep方法
7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使。但是一旦程序发生死锁,程序将死掉
生产者/消费者模式
生产/消费者问题是个非常典型的多线程问题,涉及到的对象包括生产者、消费者、仓库和产品
他们之间的关系如下:
- 生产者仅仅在仓储未满时候生产,仓满则停止生产。
- 消费者仅仅在仓储有产品时候才能消费,仓空则等待。
- 当消费者发现仓库没产品可消费时候会通知生产者生产。
- 生产者在生产出可消费产品时候,应该通知等待的消费者去消费。
生产者和消费者在同一时间段内共用同一个存储空间,生产者往存储空间中添加产品,消费者从存储空间中取走产品,当存储空间为空时,消费者阻塞,当存储空间满时,生产者阻塞。
public class Basket {
private Object data;//数据
public synchronized void product(Object data) { //要点1:生产和消费两个方法互斥
while(this.data!=null) { //要点2:如果条件不满则阻塞
this.wait(); //不能使用sleep
}
this.data=data;
this.notifyAll(); //要点3:唤醒所有通过wait方法阻塞的线程
}
public synchronized void consume() {
while(this.data==null) {
this.wait();
}
this.data=null;
this.notifyAll();
}
}
为什么要使用生产者/消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这种生产消费能力不均衡的问题,所以便有了生产者和消费者模式。
生产者/消费者模型优点
1、解耦。因为多了一个缓冲区,所以生产者和消费者并不直接相互调用,这一点很容易想到,这样生产者和消费者的代码发生变化,都不会对对方产生影响,这样其实就把生产者和消费者之间的强耦合解开,变为了生产者和缓冲区/消费者和缓冲区之间的弱耦合
2、通过平衡生产者和消费者的处理能力来提高整体处理数据的速度,这是生产者/消费者模型最重要的一个优点。如果消费者直接从生产者这里拿数据,如果生产者生产的速度很慢,但消费者消费的速度很快,那消费者就得占用CPU的时间片白白等在那边。有了生产者/消费者模型,生产者和消费者就是两个独立的并发体,生产者把生产出来的数据往缓冲区一丢就好了,不必管消费者;消费者也是,从缓冲区去拿数据就好了,也不必管生产者,缓冲区满了就不生产,缓冲区空了就不消费,使生产者/消费者的处理能力达到一个动态的平衡
生产者/消费者模式的作用
- 支持并发
- 解耦
- 支持忙闲不均
调用wait/notify之类的方法要求必须在当前线程对象内部,例如synchronized方法中
练习题
练习1:三个售票窗口同时出售20张票
package shoupiao;
public class Shou extends Thread{
private String name;
private static int piao=20;
private static Object lock=new Object();
public Shou(String name) {
this.name=name;
}
@Override
public void run() {
while (piao>0){
try {
sleep(200);
}catch (InterruptedException e){
e.printStackTrace();
}
}
synchronized (lock){
if (piao>0){
System.out.println(this.name+"售出第"+piao+"张票");
piao--;
}else {
System.out.println("票已售尽...");
}
}
}
}
练习2:两个人AB通过一个账户A在柜台取钱和B在ATM机取钱
- 分析:两个人AB实际上就是两个线程,操作同一个账号实际上为了避免出现问题则必须使用锁
public class Account {
private double balance=10000;
public synchronized void sub(double amount) {
if (balance > amount) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance = balance - amount;
}
}
}
public class Sub extends Thread {
private Account account;
private double amount;
public Sub(Account a, double am) {
this.account = a;
this.amount = am;
}
@Override
public void run() {
account.sub(amount);
}
}
练习3:过山洞问题:
请按要求编写多线程应用程序,模拟多个人通过一个山洞:
1、这个山洞每次只能通过一个人,每个人通过山洞的时间为2秒;
2、随机生成10个人,同时准备过此山洞,并且定义一个变量用于记录通过人的信息
package shandong;
//临界资源,多个线程穿越一个山洞
public class ShanDong {
//记录日志
private StringBuffer logs=new StringBuffer();
private int num=0;//到达山洞的人
private int count=0;//穿过山洞的人
private static final Object lock=new Object();//锁
public void guoshandong(){
//到达山洞口
logs.append("第"+(++num)+"人"+Thread.currentThread().getName()+"到达山洞口\n");
synchronized (lock){
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
logs.append("第"+(++count)+"人"+Thread.currentThread().getName()+"穿过山洞\n");
}
}
public void show(){
System.out.println(logs);
}
}
package shandong;
import java.util.Random;
public class Test {
public static void main(String[] args) {
String[] arr=new String[]{"LL","MM","NN","BB","VV","CC","XX","ZZ","KK","HH"};
Thread[] ts=new Thread[arr.length];
Random r=new Random();
ShanDong shan=new ShanDong();
for (int i=0;i< arr.length;i++){
String name=null;
while (name==null){
int pos=r.nextInt(arr.length);
String temp=arr[pos];
if (temp!=null){
name=temp;
arr[pos]=null;
}
}
ts[i]=new Thread(()->{
shan.guoshandong();
},name);
ts[i].start();
}
for (Thread temp:ts){
if (temp!=null){
try {
temp.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
shan.show();
}
}
练习4:模拟实现乌龟和兔子赛跑
总长2000米
规则:兔子每 0.1 秒 5 米的速度,每跑20米休息1秒
乌龟每 0.1 秒跑 2 米,不休息
当有一方到达终点时,另外一方立即终止。
package guitu_run;
//动物类
public abstract class Animal extends Thread{
public static final int len=200;//总长度
protected int lenth =len;//剩余长度
protected overI end=null;
public void setEnd(overI end) {
this.end = end;
}
@Override
public void run() {
while (lenth>0){
running();
}
}
public abstract void running();
static interface overI{
void over();
}
}
package guitu_run;
//兔子
public class Rabbit extends Animal{
public void running(){
int dis=5;
lenth-=dis;
System.out.println("兔子跑了"+dis+"米,距离终点还有"+lenth+"米");
if (lenth<=0){
lenth=0;
System.out.println("兔子胜利");
if (end!=null){
end.over();
}
}
try {
if ((len-lenth)%20==0)
sleep(1100);
else sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package guitu_run;
//乌龟
public class Tortoise extends Animal{
public void running() {
int dis=2;
lenth-=dis;
System.out.println("乌龟跑了"+dis+"米,距离终点还有"+lenth+"米");
if (lenth<=0){
lenth=0;
System.out.println("乌龟胜利");
if (end!=null){
end.over();
}
}
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package guitu_run;
public class OverImpl implements Animal.overI{
private Thread otherThread;
public OverImpl(Thread otherThread) {
this.otherThread = otherThread;
}
@Override
public void over() {
if (otherThread!=null)
otherThread.stop();
}
}
package guitu_run;
//测试类
public class Test {
public static void main(String[] args) {
Rabbit tuzi=new Rabbit();
Tortoise wugui=new Tortoise();
wugui.setEnd(new OverImpl(tuzi));
tuzi.setEnd(new OverImpl(wugui));
tuzi.start();
wugui.start();
}
}
单例模式Singleton
保证在VM中只有一个实例
- 饿汉模式
- 私有构造器
- 私有的静态属性 private static Singleton instance=new Singleton();
- 共有的静态方法
- 懒汉模式
- 私有构造器
- 私有的静态属性,不直接创建对象
- 共有的静态方法
public class Singleton {
private Singleton() {}
private static Singleton instance;
public static Singleton getInstance() {
if(instance==null)
instance=new Singleton(); //当第一次使用对象时才进行创建
return instance;
}
}
有可能会有对象的多次创建,如何解决
public class Singleton {
private Singleton() {}
private static Singleton instance;
public synchronized static Singleton getInstance() {
if(instance==null)
instance=new Singleton(); //当第一次使用对象时才进行创建
return instance;
}
}
一般不建议使用颗粒度较大的锁处理机制,并发性会受到影响
双检测的懒汉模式
-
按需创建对象,避免没有用处的创建操作
-
线程安全
public class Singleton{
private Singleton(){}
private static Singleton instance=null;
public static Singleton getInstance(){
if(instance==null){
synchronized(Singleton.class){
if(instance==null)
instance=new Singleton();
}
}
return instance;
}
}
练习题:
建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC
主要的思想就是:为了控制执行的顺序,必须要先持有prev锁,也就前一个线程要释放自身对象锁,再去申请自身对象锁,两者兼备时打印,之后首先调用self.notify()释放自身对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒
Lock的使用
Lock是java 1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制。本质上Lock仅仅是一个接口,可以通过显式定义同步锁对象来实现同步,能够提供比synchronized更广泛的锁定操作,并支持多个相关的Condition对象
- void lock();尝试获取锁,获取成功则返回,否则阻塞当前线程
void lockInterruptibly() throws InterruptedException;尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常
boolean tryLock();尝试获取锁,获取锁成功则返回true,否则返回false
boolean tryLock(long time, TimeUnit unit)尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常
- void unlock();释放锁
- Condition newCondition();返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量
Lock有三个实现类,一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。
使用方法:多线程下访问(互斥)共享资源时, 访问前加锁,访问结束以后解锁,解锁的操作推荐放入finally块中。
private final ReentrantLock lock=new ReentrantLock();
在具体方法中lock.lock() try{}finally{lock.unlock}
Lock lock=new ReentrantLock();//构建锁对象
try{
lock.lock();//申请锁,如果可以获取锁则立即返回,如果锁已经被占用则阻塞等待
System.out.println(lock);//执行处理逻辑
} finally{
lock.unlock();//释放锁,其它线程可以获取锁
}
样例1:启动4个线程,对一个int数字进行各50次加减操作,要求2个加,2个减,保证输出的线程安全
public class OperNum {
private int num=0;
private final static Lock lock=new ReentrantLock(); //构建锁对象
public void add() {
try {
lock.lock(); //申请加锁操作,如果能加上则立即返回,否则阻塞当前线程
num++;
System.out.println(Thread.currentThread().getName()+"add..."+num);
} finally {
lock.unlock(); //具体实现采用的是重入锁,所以持有锁的线程可以多次申请同一个锁,但是申请加锁次数必须和释放锁的次数一致
}
}
public void sub() {
try {
lock.lock();
num--;
System.out.println(Thread.currentThread().getName()+"sub..."+num);
} finally {
lock.unlock();
}
}
}
Condition接口
Condition是j.u.c包下提供的一个接口。可以翻译成 条件对象,其作用是线程先等待,当外部满足某一条件时,在通过条件对象唤醒等待的线程。
void await() throws InterruptedException;让线程进入等待,如果其他线程调用同一Condition对象的notify/notifyAll,那么等待的线程可能被唤醒。释放掉锁
void signal();唤醒等待的线程
void signalAll();唤醒所有线程
练习题:
写2个线程,其中一个打印1-52,另一个打印a-z,打印顺序应该是12a34b56c……5152
- 线程1执行2次数后,线程2执行1次输出
public class T1 {
private static final Lock lock = new ReentrantLock();
private static final Condition conNum = lock.newCondition();
private static final Condition conChar = lock.newCondition();
private boolean flag = false;
public void printNum() {
try {
lock.lock();
for (int i = 1; i <= 52; i++) {
while (flag)
try {
conNum.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(i);
if (i % 2 == 0) {
flag = !flag;
conChar.signal();
}
}
} finally {
lock.unlock();
}
}
public void printChar() {
try {
lock.lock();
for (int i = 'a'; i <= 'z'; i++) {
while (!flag)
try {
conChar.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print((char)i);
flag = !flag;
conNum.signal();
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
T1 tt=new T1();
new Thread(()->{
tt.printNum();
}).start();
new Thread(()->{
tt.printChar();
}).start();
}
}
使用Condition的特殊点:
- 当调用condition.await()阻塞线程时会自动释放锁,不管调用了多少次lock.lock(),这时阻塞在lock.lock()方法上线程则可以获取锁
- 当调用condition.signal()唤醒线程时会继续上次阻塞的位置继续执行,默认会自动重新获取锁(注意和阻塞时获取锁的次数一致)