二、修改代码
三、运行结果
四、原因分析
五、解决方式
1、同步互斥锁/对象监视器 synchronized
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。要
解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供 了同步机制:
synchronized来解决。它可以在我们多个线程需要访问的共享资源上进行上锁,线程A没有运行完锁里面的内容
之前,线程B是无法进入的,从而保证了线程安全。
synchronized 能做到的是 让一个线程中运行顺序是阻塞的 也就是排队的
多个线程不行
2、互斥锁的使用方式
➢隐式锁
●代码块上锁
●方法上锁
➢显式锁
● lock.上锁
六、代码块加锁
1、使用方式
synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
2 、同步锁的解释
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁,同步锁的要求:
1.锁对象可以是任意类型,我们一-般 直接使用Object对象。
2.要保证这些线程对象使用的是同一把锁。
注意:
1.在任何时候最多允许一个线程拥有 同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
2.对于非static方法,同步锁就是this。对于static方 法,我们使用当前方法所在类的字节码对象(类名.class)。
3、演示
public class ThreadSafeDemo1 {
public static void main(String[] args) {
//构建一个Runbale的子类对象
Thread2 thread = new Thread2();
//创建多个Thread对象,然后让他们共同使用同一个Runnable对象
Thread t1 = new Thread(thread,"窗口A");
Thread t2 = new Thread(thread,"窗口B");
Thread t3 = new Thread(thread,"窗口C");
//启动这三个线程
t1.start();
t2.start();
t3.start();
}
}
class Thread2 implements Runnable{
//设置票数为10张
private int ticket = 100;
//1、创建一个对象锁资源
Object lock = new Object();
@Override
public void run() {
//设置一个死循环,不断进行卖票的动作
while(true) {//公平锁和非公平锁逻辑
//2、找到会出现安全问题的代码块,进行代码块的加锁处理
synchronized (lock) {//synchronized (new Object()) {
//如果票数大于0,那么继续卖票
if(ticket>0) {
//模拟卖票的业务逻辑所需要消耗的时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖了:"+this.ticket--+"这张票");
}else {
break;
}
}
}
}
}
3、原理分析
synchronized关键字的参数里面所放的对象是一个锁对象, 同时也是几个多线程对象共同需要使用的对象,这个对象有个特点,一旦被其中一一个线程拿到之后,除非该线程执行完了这个锁里的逻辑代码(也可以通过其他手段,暂时先不讲),否则这个锁对象会一-直被该线程 占据,无法被释放,那么其他线程哪怕拿到了CPU的使用权,在没有拿到锁对象之前,也无法进入到该业务逻辑代码中来,只能被阻塞(BL OCKED),从而保证了业务逻辑代码在同一时间只能被一一个线程执行,保证了其数据的安全性!
4、场景模拟
现在有1-100张票分别有窗口A,B,C来进行售卖,它们都在卖这1 100张票,由于多个线程使用同-个共享资源,会导致安全问题,于是我们进行加锁,现在先由窗口A线程抢夺到了CPU使用权,然后开始执行窗口A这个线程的run方法,在运行同步代码块的时候,发现锁对象还在那,于是拿走锁对象,运行逻辑代码,一旦逻辑代码中被休眠,那么此时窗口A让出CPU使用权,然后窗口B抢夺到了CPU使用权,开始执行run方法,当窗口A运行到同步代码块的时候,发现锁对象不在那儿,没办法,只能进入阻塞状态,等待线程A释放锁对象出来,而线程A从休眠中醒来,运行完了同步代码块的代码时,把这个锁对象释放回去,那么在同步代码块处阻塞的线程一旦拿到了CPU使用权,此时就可以拿走锁对象,从而进入到同步代码块中来运行业务逻辑,没拿到的继续阻塞!
七、方法加锁
1、使用方式
使用synchronized修饰的方法就叫做同步方法,保证一个线程执行该方 法的时候,其他线程只能在
方法外等着。
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
2、普通方法
- 通过进行在方法结构上进行同步加锁操作
- 1、直接把synchronized写在修饰词的位置即可,代表对整个方法进行加锁
- 2、如果你的方法是一个普通方法的话,它的锁对象就是this
- 3、如果你的方法是一个静态方法的话,它的锁对象就是当前类对象.class
public class ThreadSafeDemo2 {
public static void main(String[] args) {
//构建一个Runbale的子类对象
Thread2 thread = new Thread2();
//创建多个Thread对象,然后让他们共同使用同一个Runnable对象
Thread t1 = new Thread(thread,"窗口A");
Thread t2 = new Thread(thread,"窗口B");
Thread t3 = new Thread(thread,"窗口C");
//启动这三个线程
t1.start();
t2.start();
t3.start();
//
ArrayList list = new ArrayList();
Vector v = new Vector();
}
}
class Thread2 implements Runnable{
//设置票数为10张
private static int ticket = 10;
@Override
public void run() {
//设置一个死循环,不断进行卖票的动作
while(true) {//公平锁和非公平锁逻辑
//非静态方法的调用
sellTicket();
}
}
//非静态方法,在方法结构上加锁,就等同于进行使用this来在代码块上加锁
private synchronized void sellTicket() {
synchronized (this) {
//如果票数大于0,那么继续卖票
if(ticket>0) {
//模拟卖票的业务逻辑所需要消耗的时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖了:"+this.ticket--+"这张票");
}
}
}
}
3、注意
一定要注意,如果我们使用代码块进行加锁的话,那么此时
锁对象就是当前调用该方法的对象,也就是this对象!
注意:尝试在run方法里输出this,以及在main方法创建Runnable
对象的地方输出Runnable对象,看是否相同。
整个方法进行加锁的话,够直接简单,如果方法里面的代码量大
的话,不推荐这种加锁方式,因为整个方法都进入同步了,会影响效
率。
4、静态方法
public class ThreadSafeDemo2 {
public static void main(String[] args) {
//构建一个Runbale的子类对象
Thread2 thread = new Thread2();
//创建多个Thread对象,然后让他们共同使用同一个Runnable对象
Thread t1 = new Thread(thread,"窗口A");
Thread t2 = new Thread(thread,"窗口B");
Thread t3 = new Thread(thread,"窗口C");
//启动这三个线程
t1.start();
t2.start();
t3.start();
//
ArrayList list = new ArrayList();
Vector v = new Vector();
}
}
class Thread2 implements Runnable{
//设置票数为10张
private static int ticket = 10;
@Override
public void run() {
//设置一个死循环,不断进行卖票的动作
while(true) {//公平锁和非公平锁逻辑
//静态方法的调用
sellStaticTicket();
}
}
//静态方法
private synchronized static void sellStaticTicket() {
synchronized (Thread2.class) {
//如果票数大于0,那么继续卖票
if(ticket>0) {
//模拟卖票的业务逻辑所需要消耗的时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖了:"+ticket--+"这张票");
}
}
}
}
}
八、lock加锁
1、使用方式
➢Lock 加锁机制是JDK1.5之后所提供的一个比synchronized代码块和synchronized方 法更广泛的锁定操作,拥有代码块/方法加锁的所有功能,除此之处更强大,更体现面向对象。
➢Lock锁也称同步锁,加锁与释放锁方法化了,如下:
●public void lock() :加同步锁。
● public void unlock() :释放同步锁。
➢Lock加锁的使用步骤
● 在成员位置使用ReentrantLock(可重入锁)类来创建一 个Lock对象
●在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
●在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁, -般情况下我们都会在finally里面调用unlock。
2、演示
public class ThreadSafeDemo3 {
public static void main(String[] args) {
//构建一个Runbale的子类对象
Thread2 thread = new Thread2();
//创建多个Thread对象,然后让他们共同使用同一个Runnable对象
Thread t1 = new Thread(thread,"窗口A");
Thread t2 = new Thread(thread,"窗口B");
Thread t3 = new Thread(thread,"窗口C");
//启动这三个线程
t1.start();
t2.start();
t3.start();
}
}
class Thread2 implements Runnable{
//设置票数为10张
private static int ticket = 10;
//1、创建一个Lock对象
Lock lock = new ReentrantLock();
@Override
public void run() {
//设置一个死循环,不断进行卖票的动作
while(true) {//公平锁和非公平锁逻辑
//2、在出现多线程安全问题代码之前开启锁,位置和之前的synchronized代码块位置保持一致
lock.lock();
try {
//如果票数大于0,那么继续卖票
if(ticket>0) {
//模拟卖票的业务逻辑所需要消耗的时间
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"卖了:"+this.ticket--+"这张票");
}else {
break;
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
//3、关锁操作
lock.unlock();
}
}
}
}
3、注意
lock的加锁和解锁是显式的,所以我们在进行解锁的时候,务必要注意是否在所有的条件下都完成了解锁,否则会
出现锁死的情况。
九、synchronized和lock的区别
➢在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的。JDK1.5之后并发包中新增了Lock接口以及相关实现类来实现锁功能。Lock是synchronized关键字的进阶, 掌握L ock有助于学习并发包中的源代码,在并发包中大量的类使用了Lock接口作为同步的处理方式。
➢虽然synchronized方法和语 句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。例如,用于遍历并发访问的数据结构的一些算法需要使用“手动”或“链锁定”:您获取节点A的锁定,然后获取节点B,然后释放A并获取C,然后释放B并获得D等。在这种场景中synchronized关键字就不那么容易实现了,使用Lock接口容易很多。
➢Lock锁可以在我们需要的地方显式的调用,或者中断,超时获取锁以及必须使用unL ock释放等更加灵活的锁操作;但是失去了synchronize隐式获取与释放的便捷性(我们大多在finally代码块中释放)。
➢lock中的锁定类是用于高级用户和高级情况的工具。-一般来说,除非您对Lock的某个高级特性有明确的需要,或者有明确的证据表明在特定情况下,同步已经成为可伸缩性的瓶颈,否则还是应当继续使用synchronized。
➢在确实需要一些synchronized所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,其他场合建议用synchronized开发,直到确实证明synchronized不合适。