同步
举个栗子
- 以买火车票为例
- 如果有四个线程一起卖票
- 不存在同步的话
public class TestTicked implements Runnable{
//有5张票
int ticked = 5;
@Override
//重写run()方法
public void run() {
for(int i=ticked;i>0;i--) {
System.out.println(Thread.currentThread().getName() + " sells " + i + " ticket");
}
}
//main方法测试
public static void main(String[] args) {
//实例化Thread对象
TestTicked tT = new TestTicked();
Thread td1 = new Thread(tT,"火车站");
Thread td2 = new Thread(tT,"老黄牛");
Thread td3 = new Thread(tT,"代理商");
Thread td4 = new Thread(tT,"12306");
//启动线程
td1.start();
td2.start();
td3.start();
td4.start();
}
}
执行结果
- 看结果,我们可以知道,每一个线程都卖了5张票,一共应该是卖了20张票
- 但是我们只有5张,这样显然是不合理的
同步代码块
synchronized(临界资源对象){//为临界资源对象加锁
//原子操作
}
- 某个子线程进入后,先上锁,将同步代码块中的代码以“独占”的方式执行完毕后,释放锁,以便下一个线程对象进入执行
- 锁要求:任意的非空对象都可以充当锁。
如果使用同步代码块卖票的话
public class TestTicked implements Runnable{
int ticket = 50;
//担任"锁",锁是什么不重要,他只是一把锁
//要求,非空对象
private String mutex = "";
@Override
public void run() {
//while循环
while (true) {
synchronized (mutex) {
if(ticket > 0) {
System.out.println(Thread.currentThread().getName()+ " sells " + ticket-- + " ticket.");
}else {
break;
}
}
}
}
//测试
public static void main(String[] args) {
TestTicked tt = new TestTicked();
Thread t1 = new Thread(tt, "火车站");
Thread t2 = new Thread(tt, "12306");
Thread t3 = new Thread(tt, "黄牛");
Thread t4 = new Thread(tt, "代理商");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
执行结果
这样就实现了,这四个共同卖票,而不是每个卖每个的
同步方法
- 非静态的同步方法的锁对象:this— 当前这个所在类的对象的地址值引用!
- 静态的同步方法和反射相关-----跟类的字节码文件对象 xxx.class
synchronized 返回值类型 方法名称(形参列表){//对当前对象(this)加锁
//代码(原子操作)
}
也使用同步方法
public class TestTicked implements Runnable{
int ticket = 500;
@Override
public void run() {
sellTicket();
}
//创建同步方法
/**
* 自定义一个同步方法,锁将是this对象
* 可能是t1, t2 , t3和t4中的任意一个对象
*/
public synchronized void sellTicket() {
while (true) {
if(ticket > 0) {
System.out.println(Thread.currentThread().getName() + " sells " + ticket--);
}else {
break;
}
}
}
public static void main(String[] args) {
TestTicked tt = new TestTicked();
Thread t1 = new Thread(tt, "火车站");
Thread t2 = new Thread(tt, "12306");
Thread t3 = new Thread(tt, "黄牛");
Thread t4 = new Thread(tt, "代理商");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
执行结果
这般使用同步方法的话,会是的t1,t2,t3,t4中的一个抢占了此次的资源,把该方法全部执行完(把票卖完)
Lock
- java.util.concurrent.locks.Lock
- juc包下的接口
- Lock接口提供一些相关的功能,比synchronized有具体的锁定操作(具体的功能:获取锁/释放锁)
- Lock不能直接实例化:具体的子实现类ReentrantLock
Lock lock = new ReentrantLock() ;
功能
public void lock():获取锁
public void unlock():试图释放锁
用法
public class SellTicket implements Runnable {
private static int tickets = 100 ;
//具体的锁对象:
Lock lock = new ReentrantLock() ; //lock
@Override
public void run() {
//模拟一只有票
while(true) {
try {
//加入锁:获取锁
lock.lock(); //---- synchronized(锁对象){}
if(tickets>0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在出售第"+
(tickets--)+"张票");
}
}finally {
//释放锁----
//释放相关的系统资源
lock.unlock();
}
}
}
}
synchronized与Lock的区别
- 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
死锁
死锁的概念
- 同步可以保证资源共享操作的正确性,但是过多同步也会产生问题。
- 例如,现在张三想要李四的画,李四想要张三的书,张三对李四说“把你的画给我,我就给你书”,李四也对张三说“把你的书给我,我就给你画”两个人互相等对方先行动,就这么干等没有结果
- 所谓死锁,就是两个线程都在等待对方先完成,造成程序的停滞,一般程序的死锁都是在程序运行时出现的。
//示例
class Zhangsan{ // 定义张三类
public void say(){
System.out.println("张三对李四说:“你给我画,我就把书给你。”") ;
}
public void get(){
System.out.println("张三得到画了。") ;
}
};
class Lisi{ // 定义李四类
public void say(){
System.out.println("李四对张三说:“你给我书,我就把画给你”") ;
}
public void get(){
System.out.println("李四得到书了。") ;
}
}
public class ThreadDeadLock implements Runnable{
private static Zhangsan zs = new Zhangsan() ; // 实例化static型对象
private static Lisi ls = new Lisi() ; // 实例化static型对象
private boolean flag = false ; // 声明标志位,判断那个先说话
public void run(){ // 覆写run()方法
if(flag){
synchronized(zs){ // 同步张三
zs.say() ;
try{
Thread.sleep(500) ;
}catch(InterruptedException e){
e.printStackTrace() ;
}
synchronized(ls){
zs.get() ;
}
}
}else{
synchronized(ls){
ls.say() ;
try{
Thread.sleep(500) ;
}catch(InterruptedException e){
e.printStackTrace() ;
}
synchronized(zs){
ls.get() ;
}
}
}
}
public static void main(String args[]){
ThreadDeadLock t1 = new ThreadDeadLock() ; // 控制张三
ThreadDeadLock t2 = new ThreadDeadLock() ; // 控制李四
t1.flag = true ;
t2.flag = false ;
Thread thA = new Thread(t1) ;
Thread thB = new Thread(t2) ;
thA.start() ;
thB.start() ;
}
}
经典问题
生产者消费者问题
需求
- 若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区,生产者将生产的产品放入缓冲区中.消费者从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区取商品,也不允许生产者向满的缓冲区放入产品
分析 - 需要四个类:生产者类,消费者类.,缓冲区类(工厂类)和测试类
- 生产者类和消费者类分别是一个线程
- 且工厂类里应有两个同步方法
生产者类
public class Produer extends Thread {
private BreadContainer bc = null;
private int num;
//构造器
public Produer(BreadContainer bc, int num) {
this.bc = bc;
this.num = num;
}
@Override
public void run() {
// 调用面包工厂的生产方法来生产面包
bc.produce(num);
}
}
消费者类
public class Consumer extends Thread {
private BreadContainer bc = null;
private int num;
//构造器
public Consumer(BreadContainer bc, int num) {
this.bc = bc;
this.num = num;
}
@Override
public void run() {
// 调用面包工厂的生产方法来生产面包
bc.consume(num);
}
}
工厂类
public class BreadContainer {
// 面包工厂的最大容量
private static final int MAX_NUM = 500;
// 面包工厂的当前面包数
private int currNum;
// 设置当前面包工厂的面包数量
public void setCurrNum(int currNum) {
if(currNum > MAX_NUM) {
currNum = MAX_NUM;
}
this.currNum = currNum;
}
/**
* 生产方法,用来生产num箱面包
* @param num
*/
public synchronized void produce(int num) {
System.out.printf("当前有面包数量为%d,尝试生产%d箱面包\n", currNum, num);
while(currNum + num > MAX_NUM) {
try {
System.out.println("仓库容量不足,暂停生产....");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
currNum += num;
System.out.printf("生产了%d箱面包,当前的面包数量为%d\n", num, currNum);
// 唤醒,随机唤醒线程队列中的某一个线程对象来进行执行
notify();
// 唤醒线程队列中的所有线程对象,只有获得了cpu资源的线程才可以直接进入运行状态
//notifyAll();
}
public synchronized void consume(int num) {
System.out.printf("当前有面包数量为%d,尝试消费%d箱面包\n", currNum, num);
while(currNum < num) {
try {
System.out.println("仓库库存不足,暂停消费....");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
currNum -= num;
System.out.printf("消费了%d箱面包,当前的面包数量为%d\n", num, currNum);
// 唤醒,随机唤醒线程队列中的某一个线程对象来进行执行
notify();
// 唤醒线程队列中的所有线程对象
// notifyAll();
}
}
测试类
public class Test {
public static void main(String[] args) {
BreadContainer bc = new BreadContainer();
bc.setCurrNum(30);
Produer p1 = new Produer(bc, 60);
Produer p2 = new Produer(bc, 80);
Produer p3 = new Produer(bc, 40);
Produer p4 = new Produer(bc, 20);
Consumer c1 = new Consumer(bc, 30);
Consumer c2 = new Consumer(bc, 40);
Consumer c3 = new Consumer(bc, 70);
Consumer c4 = new Consumer(bc, 50);
p1.start();
p2.start();
p3.start();
p4.start();
c1.start();
c2.start();
c3.start();
c4.start();
}
}
执行结果
sleep()和wait()对比
共同点:
- 都会抛出InterruptedException中断异常
不同点
sleep - 休眠指定时间后自动进入可运行状态
- 可以有一参、两参的重载方法
- Thread类的静态方法,只能通过Thread.sleep()来调用
- sleep不释放线程锁
wait
- 等待,需要被唤醒才能进入可运行状态,一般与notify或者notifyAll搭配使用
- 可以有无参,一参和两参的重载方法
- Object类的非静态方法,所以在任何地方都可以随时调用
- wait会释放线程锁