Java笔记-多线程之线程同步

线程同步的引入

问:为何要使用同步?

答:Java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。


案例:电影院售票

需求:某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。

通过售票案例我们看到结果出现负票的情况

因为三个线程同时执行,互相抢CPU的执行权。在进行判断时,三个线程都进入了判断语句,同时去对票数进行减减操作,这样就会出现减减多次变成负数的情况,那么应该怎么解决?

答:我们应该学习开关门原则,一旦有线程判断完,进去执行卖票,其他线程无法再进行判断进去卖票,把卖票的代码锁起来。同一时刻只能有一个线程进去执行,等这个线程执行完了,其他线程才能进去判断执行。


同步方式

1.同步代码块

即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。

代码如:

synchronized(object){
	//加锁(同步)部分
	//同一个时刻只能有一个线程进来执行
}

锁对象问题(object):只要保证唯一就可以

常见的锁对象:
自己搞一个类(接口)全是静态常量(必须是对象)。
类对象。
当前类的静态属性。
等等…

注:同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

注:在Thread的子类中,this无法充当锁对象。
在Runnable接口的实现类中,同步代码块的锁对象可以使用this,只要保证该实现类new一次取使用即可。


电影院售票代码实例:

1.继承Thread实现例子代码:

1.SellTicketThread.java:

public class SellTicketThread extends Thread {
	// static修饰变量:所有对象调用这个共同的属性,一改全改
	static int ticket = 100;

	public SellTicketThread(String name) {
		super(name);
	}

	@Override
	public void run() {
		System.out.println(getName() + "-->开始售票了!");
		while (true) {
			//同步代码块要放在循环内部
			synchronized (Math.class) {
				if (ticket > 0) {
					try {
						sleep(200);
						System.out.println(getName() + "卖出第 " + (ticket--) + " 张票");
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				} else {
					break;
				}
			}
		}
		System.out.println(getName() + "停止售票!!!!!!");
	}
}

2.Test.java:

public class Test {
	public static void main(String[] args) {
		SellTicketThread mt1 = new SellTicketThread("窗口1");
		SellTicketThread mt2 = new SellTicketThread("窗口2");
		SellTicketThread mt3 = new SellTicketThread("窗口3");
		mt1.start();
		mt2.start();
		mt3.start();
	}
}

2.实现Runable接口例子代码:

1.SellTicketThread.java:

public class SellTicketThread implements Runnable {
	int ticket = 100;

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + "-->开始售票了!");
		while (true) {
			// 同步代码块中的锁对象可以用this
			// 因为子线程是实现了Runable接口
			synchronized (this) {
				if (ticket > 0) {
					try {
						Thread.sleep(200);
						System.out.println(Thread.currentThread().getName() + "卖出第 " + (ticket--) + " 张票");
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				} else {
					break;
				}
			}
		}
		System.out.println(Thread.currentThread().getName() + "停止售票!!!!!!");
	}
}

2.Test.java:

public class Test {
	public static void main(String[] args) {
		SellTicketThread stt = new SellTicketThread();
		Thread t1 = new Thread(stt, "窗口1");
		Thread t2 = new Thread(stt, "窗口2");
		Thread t3 = new Thread(stt, "窗口3");
		t1.start();
		t2.start();
		t3.start();
	}
}

2.同步方法

即有synchronized关键字修饰的方法。

由于Java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

代码如:
public synchronized void go(){}

同步方法默认的锁对象就是this,所以普通的同步方法在Thread子类里面不能用。

静态同步方法,锁对象默认是当前类的类对象(类名点class) 。

所以在Thread子类里面想要用同步方法,那么,同步方法必须用static修饰,并且锁对象用当前类的类对象。

Runnable实现类里面,同步方法可以不用加static,直接用this作为锁对象即可 。

注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。


电影院售票代码实例:

1.继承Thread实现例子代码:

1.SellTicketThread.java:

public class SellTicketThread extends Thread {
	static int ticket = 100;

	public SellTicketThread(String name) {
		super(name);
	}

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + "-->开始售票了!");
		while (true) {
			boolean flag = sell();
			if (!flag) {
				break;
			}
		}
		System.out.println(Thread.currentThread().getName() + "停止售票!!!!!!");

	}

	// Thread子类的同步方法必须用Static修饰
	public static synchronized boolean sell() {
		// 用来判断100张票是否卖完;卖完-->false;没卖完-->true
		boolean flag = false;
		if (ticket > 0) {
			try {
				Thread.sleep(100);
				System.out.println(Thread.currentThread().getName() + "卖出第 " + (ticket--) + " 张票");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			flag = true;
		}
		return flag;
	}
}

2.Test.java:

public class Test {
	public static void main(String[] args) {
		SellTicketThread t1 = new SellTicketThread("窗口1");
		SellTicketThread t2 = new SellTicketThread("窗口2");
		SellTicketThread t3 = new SellTicketThread("窗口3");
		t1.start();
		t2.start();
		t3.start();
	}
}

2.实现Runable接口例子代码:

演示代码如下:

1.SellTicketThread:

public class SellTicketThread implements Runnable {
	// 无需Static修饰
	int ticket = 100;

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + "-->开始售票了!");
		while (true) {
			boolean flag = sell();
			if (!flag) {
				break;
			}
		}
		System.out.println(Thread.currentThread().getName() + "停止售票!!!!!!");

	}

	// 无需Static修饰
	public synchronized boolean sell() {
		// 用来判断100张票是否卖完;卖完-->false;没卖完-->true
		boolean flag = false;
		if (ticket > 0) {
			try {
				Thread.sleep(100);
				System.out.println(Thread.currentThread().getName() + "卖出第 " + (ticket--) + " 张票");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			flag = true;
		}
		return flag;
	}
}

2.Test.java:

public class Test {
	public static void main(String[] args) {
		SellTicketThread stt = new SellTicketThread();
		Thread t1 = new Thread(stt, "窗口1");
		Thread t2 = new Thread(stt, "窗口2");
		Thread t3 = new Thread(stt, "窗口3");
		t1.start();
		t2.start();
		t3.start();
	}
}

Lock锁同步

介绍

在JDK1.5中新增了一个java.util.concurrent包来支持同步。
ReentrantLock类是可重入、互斥、实现了Lock接口的锁,
它与使用synchronized方法和``synchronized代码块具有相同的基本行为和语义,并且扩展了其能力。

ReenreantLock类的常用方法有:

ReentrantLock(): 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁

注意:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用。

Lock锁跟同步代码块的区别::

1.同步代码块和同步方法:没有明显看出,在哪里加锁,在哪里释放锁。
而Lock锁:加锁解锁明显。

2.一旦程序有报错,同步代码块会自动释放锁;
lock锁不会自动释放锁,一旦进入到同步代码部分,报错终止,那么这个锁一辈子无法打开,其他线程根本进不去。

所以:我们的lock.unlock()这句代码,必须执行,无论什么情况都得执行,必须必须执行!!!!

所以我们的lock 需要跟try finally结合使用。

关于 Lock 对象和 synchronized 关键字的选择:

a.最好两个都不用,使用一种java.util.concurrent包提供的机制,
能够帮助用户处理所有与锁相关的代码。

b.如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码

c.如果需要更高级的功能,就用ReentrantLock()类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁。

try{
	lock.lock();
	//被同步的代码
}finally{
	lock.unlock();
}

例如:
在上面例子的基础上,改写后的代码为:

1.继承Thread实现例子代码:

1.MyThread.java:

public class MyThread extends Thread {
	public static int tickets = 100;
	public static Lock myLock = new ReentrantLock();

	public MyThread(String name) {
		super(name);
	}

	@Override
	public void run() {
		System.out.println(getName() + "开始售票!");
		while (true) {
			// 加锁
			myLock.lock();
			try {
				if (tickets > 0) {
					try {
						sleep(100);
						System.out.println(getName() + "--->售出了第:" + (tickets--) + "张票!");
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				} else {
					System.out.println(getName() + "停止售票!!!!");
					break;
				}
			} finally {
				// 解锁
				myLock.unlock();
			}
		}
	}
}

2.Test.java:

public class Test {
	public static void main(String[] args) {
		MyThread t1 = new MyThread("窗口1");
		MyThread t2 = new MyThread("窗口2");
		MyThread t3 = new MyThread("窗口3");
		t1.start();
		t2.start();
		t3.start();
	}
}

2.实现Runable接口例子代码:

1.MyThread.java:

public class MyThread implements Runnable {
	public int tickets = 100;
	public Lock myLock = new ReentrantLock();

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + "开始售票!");
		while (true) {
			// 加锁
			myLock.lock();
			try {
				if (tickets > 0) {
					try {
						Thread.sleep(100);
						System.out.println(Thread.currentThread().getName() + "--->售出了第:" + (tickets--) + "章票!");
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				} else {
					System.out.println(Thread.currentThread().getName() + "停止售票!!!!");
					break;
				}
			} finally {
				// 解锁
				myLock.unlock();
			}
		}
	}
}

2.Test.java:

public class Test {
	public static void main(String[] args) {
		MyThread t = new MyThread();
		Thread t1 = new Thread(t, "窗口1");
		Thread t2 = new Thread(t, "窗口2");
		Thread t3 = new Thread(t, "窗口3");
		t1.start();
		t2.start();
		t3.start();
	}
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值