线程安全问题与线程同步
1、什么是线程安全问题
之前学习常用类时:
StringBuffer与StringBuilder
ArrayList与Vector
HashMap与Hashtable
和上一篇的卖票问题,出现负数票
导致线程安全问题:多个线程访问同一份资源
问题出现总结:
1、多个线程
2、共享数据
3、多条语句访问或操作共享数据
2、如何解决线程安全问题
解决思路:
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
解决办法:
如果想要解决线程安全问题,就必须使用同步。所谓的同步就是指多个操作在同一个时间段内只能有一个线程进行,其他线程要等待
此线程完成之后才可以继续执行。
3、同步的几种方式
(1)同步代码块
格式:多个线程需要使用唯一的一把锁
synchronized(锁对象){
需要同步的代码
}
(2)同步方法
格式:synchronized 方法其他修饰符 返回值类型 方法名(形参列表)throws 异常列表{}
1.同步代码块
package com.synch;
public class TestTicket {
public static void main(String[] args) {
Ticket t = new Ticket();
//三个线程共享了t对象,num就只有一份,每个线程都可能修改它,一旦其中一个线程修改它,那么就会影响其他线程
new Thread(t,"窗口一").start();
new Thread(t,"窗口二").start();
new Thread(t,"窗口三").start();
}
}
class Ticket implements Runnable{
private int num = 10;
public void run(){
while(true){
synchronized (this) {
if(num<=0){
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了一张票,剩余:"+(--num));
}
}
}
}
2.同步方法
package com.synch;
public class TestTicket {
public static void main(String[] args) {
Ticket t = new Ticket();
//三个线程共享了t对象,num就只有一份,每个线程都可能修改它,一旦其中一个线程修改它,那么就会影响其他线程
new Thread(t,"窗口一").start();
new Thread(t,"窗口二").start();
new Thread(t,"窗口三").start();
}
}
class Ticket implements Runnable{
private int num = 10;
private boolean flag =true;
public void run(){
while(flag){
sale();
}
}
synchronized public void sale(){
if(num<=0){
flag = false;
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了一张票,剩余:"+(--num));
}
}
4、同步需要注意的问题
(1)同步锁
同步锁机制:
在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。
防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁
之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。
synchronized的锁是什么?
任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
同步方法的锁:静态方法(类名.class)、非静态方法(this)
同步代码块:自己指定,很多时候也是指定为this或类名.class
注意:
必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)
(2)同步的范围
如何找问题,即代码是否存在线程安全?(非常重要)
(1)明确哪些代码是多线程运行的代码
(2)明确多个线程是否有共享数据
(3)明确多线程运行代码中是否有多条语句操作共享数据的
2、如何解决呢?(非常重要)
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
即所有操作共享数据的这些语句都要放在同步范围中
切记:
范围太小:没锁住所有有安全问题的代码
范围太大:没发挥多线程的功能
public class TestSynchProblem {
public static void main(String[] args) {
Window t = new Window();
//三个线程共享了t对象,num就只有一份,每个线程都可能修改它,一旦其中一个线程修改它,那么就会影响其他线程
new Thread(t,"窗口一").start();
new Thread(t,"窗口二").start();
new Thread(t,"窗口三").start();
}
}
class Window implements Runnable{
private int num = 10;
//范围太大,导致只有一个窗口可以卖票
public void run(){
while(true){
//多个线程不是同一把锁
synchronized(new Object()){//synchronized((Integer)num){[ int num = 10;
Integer i = (Integer)num;
num = 9;
Integer j = (Integer)num;
System.out.println(i==j);//false]
if(num<=0){
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了一张票,剩余:"+(--num));
}
}
}
}
class Window implements Runnable{
private int num = 10;
public void run(){
while(num>0){//num>0的条件也属于访问共享数据的代码,没有放入同步范围,范围太小,数据仍然不安全
synchronized (this) {
//if(num>0){//解决方案,可以在里面再判断一次条件
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了一张票,剩余:"+(--num));
// }else{
// break;
// }
}
}
}
}
class Window implements Runnable{
private int num = 10;
//范围太大,导致只有一个窗口可以卖票
synchronized public void run(){
while(num>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了一张票,剩余:"+(--num));
}
}
}
(3)同步的优点与缺点
同步的好处:解决了多线程的安全问题
副作用:多个线程需要等待锁,因此性能低
5、释放锁和不释放锁
释放锁的操作
当前线程的同步方法、同步代码块执行结束。
当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致当前线程异常结束。
当前线程在同步代码块、同步方法中执行了锁对象的wait()方法,当前线程被挂起,并释放锁。
不会释放锁的操作
1.线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
2.线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该该线程挂起,该线程不会
释放锁(同步监视器)。
1.应尽量避免使用suspend()和resume()这样的过时来控制线程
6、死锁(遏制彼此命门,死活不放)
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
解决方法:
专门的算法、原则
尽量减少同步资源的定义
尽量避免嵌套同步
7、显示锁Lock
java从1.5版本之后,提供了Lock接口。在后期对锁的分析过程中,发现,获取锁,或者释放锁的动作应该是锁这个事物更清楚。
所以将这些动作定义在了锁当中,并把锁定义成对象。所以,同步是隐式的锁操作,而Lock对象是显示的锁操作。
synchronized是JVM层面提供的锁,而Lock是Java的语言层面jdk为我们提供的锁,这些锁都在 Java.util.concurrent包中。
先来看一下JVM提供的锁和并发包中的锁有哪些区别:
1.synchronized的加锁和释放都是由JVM提供,不需要我们关注,而lock的加锁和释放全部由我们去控制,
通常释放锁的动作要在finally中实现。
2.synchronized只有一个状态条件,也就是每个对象只有一个监视器,如果需要多个Condition的组合那么synchronized
是无法满足的,而Lock则提供了多条件的互斥,非常灵活。
3.ReentrantLock(可重入锁,锁的一种) 拥有Synchronized相同的并发性和内存语义,
此外还多了锁投票,定时锁等候和中断锁等候。(锁的种类有很多)
package com.lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestLock {
public static void main(String[] args) {
Ticket t = new Ticket();
//三个线程共享了t对象,num就只有一份,每个线程都可能修改它,一旦其中一个线程修改它,那么就会影响其他线程
new Thread(t,"窗口一").start();
new Thread(t,"窗口二").start();
new Thread(t,"窗口三").start();
}
}
class Ticket implements Runnable{
private int num = 10;
private final ReentrantLock lock = new ReentrantLock();
public void run(){
while(true){
try {
lock.lock();//获取锁
if(num<=0){
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了一张票,剩余:"+(--num));
} catch (Exception e) {
e.printStackTrace();
}finally{
lock.unlock();//释放锁
}
}
}
}
idea枷锁方式快捷键:ctrl+alt+t