今天学习的内容是同步和死锁
一、同步
通过之前的学习我们知道,多个线程可以共享同一个JVM进程的资源。但你永远都不知道一个线程何时在运行,这就有可能出现问题:当多个线程同时访问共享资源时,就有可能造成线程之间的冲突,或者称为线程安全问题。实际上,当一个线程在执行操作共享数据的多条代码时,其它线程也执行了操作共享数据的代码,这时就会产生线程安全问题。首先来看下面的例子:
public class TicketsDemo {
public static void main(String[] args){
Runnable mission = new Ticket();
Thread t1 = new Thread(mission,"1");
Thread t2 = new Thread(mission,"2");
Thread t3 = new Thread(mission,"3");
Thread t4 = new Thread(mission,"4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Ticket implements Runnable{
private int number=10;
public void run(){
sell();
}
public void sell(){
while(true){
if(number>0){
try{
Thread.sleep(200);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"号窗口卖出一张票,剩余票数:"+(--number));
}
}
}
}
那么如何解决线程安全问题呢?答案就是Java同步机制:将操作共享资源的代码“锁起来”,在当前线程执行完这段代码之前,其他想要执行这段代码的线程都会进入阻塞状态。可以使用synchronized关键字将操作共享数据的代码封装为一个同步代码块,或者封装成一个用synchronized关键字修饰的同步方法。下面使用同步代码块解决上述问题:
public class TicketsDemo {
public static void main(String[] args){
Runnable mission = new Ticket();
Thread t1 = new Thread(mission,"1");
Thread t2 = new Thread(mission,"2");
Thread t3 = new Thread(mission,"3");
Thread t4 = new Thread(mission,"4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Ticket implements Runnable{
private int number=10;
public void run(){
sell();
}
public void sell(){
while(true){
//同步代码块
synchronized (this) {
if(number>0){
try{
Thread.sleep(200);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"号窗口卖出一张票,剩余票数:"+(--number));
}
}
}
}
}
程序运行结果:
OK!问题解决了!总结一下同步的知识点:
- 同步代码块的格式:synchronized(对象){需要同步的代码}
- 同步方法的格式: synchronized 返回值类型 方法名(){}
- 同步代码块和同步方法中的代码称为同步代码
- 同步的原理:所有对象都自动含有一把锁和一把钥匙,当线程A访问某一对象的任意同步代码时,会将钥匙拿走,并将该对象的所有同步代码都锁上。此时其它线程没有钥匙,无法访问此对象的同步代码,只能进入阻塞状态,等待线程A执行完毕
- 同步代码块的对象参数指定了使用哪个对象的锁,通常操作谁的资源用谁的锁(比如Servlet中的this.getServletContext(),操作ServletContext对象的资源,就用ServletContext的锁)
- 同步方法一定使用this的锁,所以同步方法可以视作对象参数为this的同步代码块的简写
- 实际开发中通常使用同步代码块(由于不一定使用this的锁)
- 静态方法也可以被同步化,使用的是类的字节码对象的锁,字节码对象可以通过this.getClass()或类名.class获取
- 同步的弊端:线程在执行同步代码之前要查询锁和钥匙的状态,造成性能上的损耗;强制线程排队执行同步代码,违背了并发的原则;可能会造成死锁
下面再看一个使用同步方法解决并发性问题的示例:
public class Test40 {
public static void main(String[] args){
Runnable r = new MyRunnable();
Thread t1 = new Thread(r,"甲");
Thread t2 = new Thread(r,"乙");
t1.start();
t2.start();
}
}
class Bank{
private int sum;
public synchronized void add(int num){//同步方法
sum = sum + num;
try{
Thread.sleep(200);
} catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("用户"+Thread.currentThread().getName()+"存完钱之后,银行金库总数为:"+sum);
}
}
class MyRunnable implements Runnable{
private Bank b = new Bank();
public void run(){
for(int i=0; i<3; i++){
b.add(100);
}
}
}
上述实例都是多个线程执行相同任务时出现的线程安全问题,其实只要多个线程都执行操作共享数据的代码,即使它们的任务不同,也会产生线程安全问题。比如多个用户的请求(每个请求独占一条线程)都操作ServletContext的属性,也会发生线程安全问题,解决这种问题的方法同样是同步,注意要将每个线程操作共享数据的代码都同步化,此方法才奏效!
如下面的程序示例:
public class ThreadCommunicationTest{
public static void main(String[] args){
Resource r = new Resource();//资源
Input in = new Input(r);//任务1
Output out = new Output(r);//任务2
Thread t1 = new Thread(in,"线程1");//线程1
Thread t2 = new Thread(out,"线程2");//线程2
t1.start();
t2.start();
}
}
//资源
class Resource{
private String name;
private String sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
//输入
class Input implements Runnable{
private Resource r;
public Input(){}
public Input(Resource r){
this.r = r;
}
public void run(){
int x = 0;
while(true){
synchronized (r) {//所有操作共享数据的代码都要同步
if(x==0){
r.setName("k");
r.setSex("男");
}else{
r.setName("q");
r.setSex("女");
}
}
x = (x+1)%2;
}
}
}
//输出
class Output implements Runnable{
private Resource r ;
public Output(){}
public Output(Resource r){
this.r = r;
}
public void run(){
while(true){
synchronized (r) {//所有操作共享数据的代码都要同步
System.out.println(r.getName()+":"+r.getSex());
}
}
}
}
二、死锁
如果在程序中存在两个线程和两把锁,并且使用同步嵌套,就可能引发死锁问题。死锁问题实质上就是两个线程都想拿到各自所持有的钥匙,僵持不下。死锁问题无法解决,所以要尽量避免同步嵌套的使用。面试重点:设计一个死锁程序:
public class DeadLockTest {
public static void main(String[] args){
DeadLock d1 = new DeadLock(true);
DeadLock d2 = new DeadLock(false);
//两个线程
Thread t1 = new Thread(d1,"1");
Thread t2 = new Thread(d2,"2");
t1.start();
t2.start();
}
}
class DeadLock implements Runnable{
private boolean flag;
public DeadLock(){}
public DeadLock(boolean flag){
this.flag = flag;
}
public void run(){
if(flag){
while(true){//为了确保发生死锁现象
synchronized (MyLock.locka) {
System.out.println("线程"+Thread.currentThread().getName()+"拿到了locka的钥匙");
System.out.println("线程"+Thread.currentThread().getName()+"准备拿lockb的钥匙");
synchronized (MyLock.lockb) {
System.out.println("线程"+Thread.currentThread().getName()+"拿到了lockb的钥匙");
}
}
}
}else{
while(true){
synchronized (MyLock.lockb) {
System.out.println("线程"+Thread.currentThread().getName()+"拿到了lockb的钥匙");
System.out.println("线程"+Thread.currentThread().getName()+"准备拿locka的钥匙");
synchronized (MyLock.locka) {
System.out.println("线程"+Thread.currentThread().getName()+"拿到了locka的钥匙");
}
}
}
}
}
}
class MyLock{
//两个锁
public static final Object locka = new Object();
public static final Object lockb = new Object();
}
程序运行结果: