Java学习之锁机制:sychronized关键字

什么是锁机制

有两种机制防止代码块受并发的干扰。java语言提供一个synchronized关键字达到这一目的,并且Java SE 5.0引入了ReentrantLock类(本文不做讨论)。synchronized关键字自动提供一个锁以及相关的条件。


锁和synchronized的关系

锁是java中用来实现同步的工具。
之所以能对方法或者代码块实现同步的原因是:
只有拿到锁的线程才能执行synchronized修饰的方法或代码块,且其它线程获得锁的唯一方法是等目前拿到锁的线程执行完方法将锁释放,如果synchronized修饰的代码块或方法没执行完是不会释放这把锁的,这就保证了拿到锁的线程一定可以一次性把它调用的方法或代码块执行完。


锁的分类

类锁

1.概念:类锁,是用来锁类的,一个类的所有对象共享一个class对象,共享一组静态方法,当synchronized指定修饰静态方法或者class对象的时候,拿到的就是类锁,类锁是所有对象共同争抢一把。
2.作用:使持有者可以同步地调用静态方法。

对象锁

1.概念:对象锁,是用来锁对象的,虚拟机为每个非静态方法和非静态域都分配了自己的空间。与静态方法和静态域不同,是所有对象共用一组。所以synchronized修饰非静态方法或者this的时候拿到的就是对象锁,对象锁是每个对象各有一把的。
下面看一个与对象锁有关的测试:
(同步代码块部分)

class Lock02{
    public void test1() {
        //对象锁1::锁定代码块
        synchronized (this) {
            int i = 10;
            while (i-- > 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //对象锁2:锁定方法
    public synchronized void test2(){
        int i =10;
        while(i-- >0){
            System.out.println(Thread.currentThread().getName()+":"+i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

(main函数部分)

public class LockTest02 {
    public static void main(String[] args) {
        Lock02 l1 = new Lock02();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                l1.test1();
            }
        },"a");

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                l1.test2();
            }
        },"b");

        t1.start();
        t2.start();

    }
}

输出结果:

b:9
b:8
b:7
b:6
b:5
b:4
b:3
b:2
b:1
b:0
a:9
a:8
a:7
a:6
a:5
a:4
a:3
a:2
a:1
a:0

解释:因为在Lock02类中分别有一个非静态方法和代码块上了“锁”,因为这两个锁均为对象锁,即拿到锁的都是线程都是当前的对象,即必须等先拿到锁的线程“b”将锁中的循环执行完毕之后才能释放锁给“a”用。
!!!
但是,如果把第二个方法前的synchronized关键字去掉的话,结果就会产生变化,去掉关键字后的运行结果为:

b:9
a:9
b:8
a:8
a:7
b:7
a:6
b:6
a:5
b:5
a:4
b:4
a:3
b:3
a:2
b:2
a:1
b:1
a:0
b:0

结果为线程b和线程a交替输出结果,原因如下:

  • 对象的内置锁和对象的状态之间没有内在关联。
  • 当获取到与对象关联的内置锁时,并不能阻止其他线程访问该对象。
  • 当某个线程获得对象锁之后,只能阻止其他线程获得同一个锁。但是对于没有上锁的部分,是不会对其它线程的访问造成影响的。

所以,虽然线程a访问的test()1持有当前对象的锁,但是,线程b访问的方法tes()2并没有上锁,是可以自由访问的,所以程序是交替输出结果的。


对象锁和类锁的使用

如果两个线程同时调用一个上锁的非静态方法(对象锁)或者静态方法(类锁),便可以实现线程同步,即结果都为一个线程执行完之后释放锁另一个线程再使用。
但是当类锁和对象锁同时存在时:

class B {
	//静态方法,上类锁,函数功能为连续打印1000个value值,调用时会传1,所以会打印1000个1
	synchronized public static void mB(String value) throws InterruptedException {
		for (int i = 0; i < 1000; i++) {
			System.out.print(value);
			Thread.sleep(100);
		}
	}
	
	public void mC(String value) {
		//修饰this上对象锁,函数功能也是连续打印1000个value值,调用时会传2,所以会打印1000个2
		synchronized (this) {
			for (int i = 0; i < 1000; i++) {
				System.out.print(value);
				Thread.sleep(100);
			}
		}
	}

	public static void main(String[] args) {

		Thread thread = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					B.mB("1");
					
				} catch (InterruptedException e) {
					e.printStackTrace();
				}

			}
		});
		B b = new B();
		Thread thread2 = new Thread(new Runnable() {
			@Override
			public void run() {
				b.mC("2");
			}
		});
		thread.start();
		thread2.start();
		
	}
}

上述情况下,类B的静态方法和代码块功能都是打印100个value值,但是静态方法是类锁,而代码块锁this,是对象锁。所以代码块和静态方法交替执行,最后结果也是交替打印。
结论:类锁和对象锁不同,它们之间不会产生互斥。


死锁(DeadLock)

1.引入:
当多个线程都被阻塞之后,有可能会因为每一个线程要等待导致所有线程都被阻塞。这样的状态称为死锁。
2.概念:
所谓死锁是指多个线程因竞争资源而造成的的一种僵持(互相等待),若无外力作用,这些进程都无法向前推进。
3.死锁产生的必要条件:

  • 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
  • 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
  • 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
  • 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。
    4.小结:
    死锁是一种特定的程序状态,在实体之间,由于循环依赖导致彼此一直处于等待之中,没有任何个体可以继续前进。死锁不仅仅是在多线程之间发生,存在资源独占的进程之间同样也可能出现死锁。通常来说,大多死锁情况是指:两个或多个线程之间,由于互相持有对方需要的锁,而处于永久阻塞的状态

实例

模拟两个人吃面包和喝牛奶的实例,一个人选择先吃面包,一个选择先喝牛奶。
这种情况下就可能造成死锁。

class Bread{

}

class Milk{

}

class Eating extends Thread{
    //此处只有是静态变量才可能造成死锁
    static  Milk milk = new Milk();
    static Bread bread = new Bread();

    private int choice;
    String name;

    public Eating(int choice, String name) {
        this.choice = choice;
        this.name = name;
    }

    public void run(){
        test();
    }

    public void test(){
        if(choice == 0){
            synchronized (milk){
                System.out.println(this.name +"milk");
//                try {
//                    Thread.sleep(1000);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
                synchronized (bread){
                    System.out.println(this.name+"bread");
                }
            }


        }else{
            synchronized (bread){
                System.out.println(this.name+"bread");
//                try {
//                    Thread.sleep(2000);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
                synchronized (milk){
                    System.out.println(this.name+"milk");
                }
            }
            
        }
        
    }
}

main方法:

public class LockTest03 {
    public static void main(String[] args) {
        Eating eating = new Eating(1,"A");

        Eating eating1 = new Eating(0,"B");
        eating.start();
        eating1.start();

    }
}

这种情况下的输出结果为:

Abread
Bmilk

ps:
在Eating类中,Milk对象和Bread对象新建时必须设置成static的属性。只有这样两个线程互相持有的锁才是同一把锁,才能造成死锁的情况。

  static  Milk milk = new Milk();
  static Bread bread = new Bread();

总结

有关锁和条件的关键之处:

1.锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
2.锁可以管理试图进入被保护代码段的线程。
3.锁可以拥有一个或多个相关条件的条件对象。
4.每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值