多线程并发安全问题
概念
当多个线程并发操作同一临界资源,由于线程切换时机不确定,导致操作临界资源的顺序出现混乱严重时可能导致系统瘫痪. 临界资源:操作该资源的全过程同时只能被单个线程完成.
例
当beans为1时,若两个线程同时调用getBean方法,t1线程先进行if判断,此时beans不为0,于是执行if后面的操作准备获取beans的值并对其进行--操作,但是还没有执行这句话发生了线程切换,那么t2线程也进行if判断,由于beans不为0,t2线程也执行if后面的操作获取beans的值并对其进行--操作,这会导致两个线程最终将beans的值从-减为了-1.导致后续操作出现死循环。
这就是由于线程切换不确定导致执行顺序出现了混乱,也就是所谓的并发安全问题
package thread;
/**
* 多线程并发安全问题
* 当多个线程并发操作同一临界资源,由于线程切换的时机不确定,导致操作顺序出现
* 混乱,严重时可能导致系统瘫痪。
* 临界资源:同时只能被单一线程访问操作过程的资源。
*/
public class SyncDemo {
public static void main(String[] args) {
Table table = new Table();
Thread t1 = new Thread(){
public void run(){
while(true){
int bean = table.getBean();
Thread.yield();
System.out.println(getName()+":"+bean);
}
}
};
Thread t2 = new Thread(){
public void run(){
while(true){
int bean = table.getBean();
/*
static void yield()
线程提供的这个静态方法作用是让执行该方法的线程
主动放弃本次时间片。
这里使用它的目的是模拟执行到这里CPU没有时间了,发生
线程切换,来看并发安全问题的产生。
*/
Thread.yield();
System.out.println(getName()+":"+bean);
}
}
};
t1.start();
t2.start();
}
}
class Table{
private int beans = 20;//桌子上有20个豆子
public int getBean(){
if(beans==0){
throw new RuntimeException("没有豆子了!");
}
Thread.yield();
return beans--;
}
}
synchronized关键字
解决并发安全问题的本质就是将多个线程并发(同时)操作改为同步(排队)操作来解决。
synchronized有两种使用方式
-
在方法上修饰,此时该方法变为一个同步方法
-
同步块,可以更准确的锁定需要排队的代码片段
同步方法
当一个方法使用synchronized修饰后,这个方法称为"同步方法",即:多个线程不能同时 在方法内部执行.只能有先后顺序的一个一个进行. 将并发操作同一临界资源的过程改为同步执行就可以有效的解决并发安全问题.
package thread;
/**
* 多线程并发安全问题
* 当多个线程并发操作同一临界资源,由于线程切换的时机不确定,导致操作顺序出现
* 混乱,严重时可能导致系统瘫痪。
* 临界资源:同时只能被单一线程访问操作过程的资源。
*/
public class SyncDemo {
public static void main(String[] args) {
Table table = new Table();
Thread t1 = new Thread(){
public void run(){
while(true){
int bean = table.getBean();
Thread.yield();
System.out.println(getName()+":"+bean);
}
}
};
Thread t2 = new Thread(){
public void run(){
while(true){
int bean = table.getBean();
/*
static void yield()
线程提供的这个静态方法作用是让执行该方法的线程
主动放弃本次时间片。
这里使用它的目的是模拟执行到这里CPU没有时间了,发生
线程切换,来看并发安全问题的产生。
*/
Thread.yield();
System.out.println(getName()+":"+bean);
}
}
};
t1.start();
t2.start();
}
}
class Table{
private int beans = 20;//桌子上有20个豆子
/**
* 当一个方法使用synchronized修饰后,这个方法称为同步方法,多个线程不能
* 同时执行该方法。
* 将多个线程并发操作临界资源的过程改为同步操作就可以有效的解决多线程并发
* 安全问题。
* 相当于让多个线程从原来的抢着操作改为排队操作。
*/
public synchronized int getBean(){
if(beans==0){
throw new RuntimeException("没有豆子了!");
}
Thread.yield();
return beans--;
}
}
同步块
有效的缩小同步范围可以在保证并发安全的前提下尽可能的提高并发效率.同步块可以更准确的控制需要多个线程排队执行的代码片段.
语法:
synchronized(同步监视器对象){ 需要多线程同步执行的代码片段 }
同步监视器对象即上锁的对象,要想保证同步块中的代码被多个线程同步运行,则要求多个线程看到的同步监视器对象是同一个.
package thread;
/**
* 有效的缩小同步范围可以在保证并发安全的前提下尽可能提高并发效率。
*
* 同步块
* 语法:
* synchronized(同步监视器对象){
* 需要多个线程同步执行的代码片段
* }
* 同步块可以更准确的锁定需要多个线程同步执行的代码片段来有效缩小排队范围。
*/
public class SyncDemo2 {
public static void main(String[] args) {
Shop shop = new Shop();
Thread t1 = new Thread(){
public void run(){
shop.buy();
}
};
Thread t2 = new Thread(){
public void run(){
shop.buy();
}
};
t1.start();
t2.start();
}
}
class Shop{
public void buy(){
/*
在方法上使用synchronized,那么同步监视器对象就是this。
*/
// public synchronized void buy(){
Thread t = Thread.currentThread();//获取运行该方法的线程
try {
System.out.println(t.getName()+":正在挑衣服...");
Thread.sleep(5000);
/*
使用同步块需要指定同步监视器对象,即:上锁的对象
这个对象可以是java中任何引用类型的实例,只要保证多个需要排队
执行该同步块中代码的线程看到的该对象是"同一个"即可
*/
synchronized (this) {
// synchronized (new Object()) {//没有效果!
System.out.println(t.getName() + ":正在试衣服...");
Thread.sleep(5000);
}
System.out.println(t.getName()+":结账离开");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在静态方法上使用synchronized
当在静态方法上使用synchronized后,该方法是一个同步方法.由于静态方法所属类,所以一定具有同步效果.
静态方法使用的同步监视器对象为当前类的类对象(Class的实例).
注:类对象会在后期反射知识点介绍.
package thread;
/**
* 静态方法上如果使用synchronized,则该方法一定具有同步效果。
*/
public class SyncDemo3 {
public static void main(String[] args) {
Thread t1 = new Thread(){
public void run(){
Boo.dosome();
}
};
Thread t2 = new Thread(){
public void run(){
Boo.dosome();
}
};
t1.start();
t2.start();
}
}
class Boo{
/**
* synchronized在静态方法上使用是,指定的同步监视器对象为当前类的类对象。
* 即:Class实例。
* 在JVM中,每个被加载的类都有且只有一个Class的实例与之对应,后面讲反射
* 知识点的时候会介绍类对象。
*/
public synchronized static void dosome(){
try {
Thread t = Thread.currentThread();
System.out.println(t.getName() + ":正在执行dosome方法...");
Thread.sleep(5000);
System.out.println(t.getName() + ":执行dosome方法完毕!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
静态方法中使用同步块时,指定的锁对象通常也是当前类的类对象
package thread;
public class SyncDemo3 {
public static void main(String[] args) {
// new Thread(()->Foo.dosome()).start();
// new Thread(Foo::dosome).start();
Foo f1 = new Foo();
Foo f2 = new Foo();
new Thread(()->f1.dosome()).start();
new Thread(()->f2.dosome()).start();
}
}
class Foo{
// public synchronized static void dosome(){
public static void dosome(){
/*
在静态方法中使用同步块时,同步监视器对象还是使用当前类的类对象
获取类对象的方式:类名.class
例如获取Foo的类对象就是:Foo.class
*/
synchronized (Foo.class) {
try {
Thread t = Thread.currentThread();
System.out.println(t.getName() + ":正在执行dosome方法");
Thread.sleep(5000);
System.out.println(t.getName() + ":执行dosome方法完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
互斥锁
当多个线程执行不同的代码片段,但是这些代码片段之间不能同时运行时就要设置为互斥的.
使用synchronized锁定多个代码片段,并且指定的同步监视器是同一个时,这些代码片段之间就是互斥的.
package thread;
/**
* 互斥锁
* 当使用synchronized锁定多个不同的代码片段,并且指定的同步监视器对象相同时,
* 这些代码片段之间就是互斥的,即:多个线程不能同时访问这些方法。
*/
public class SyncDemo4 {
public static void main(String[] args) {
Foo foo = new Foo();
Thread t1 = new Thread(){
public void run(){
foo.methodA();
}
};
Thread t2 = new Thread(){
public void run(){
foo.methodB();
}
};
t1.start();
t2.start();
}
}
class Foo{
public synchronized void methodA(){
Thread t = Thread.currentThread();
try {
System.out.println(t.getName()+":正在执行A方法...");
Thread.sleep(5000);
System.out.println(t.getName()+":执行A方法完毕!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void methodB(){
Thread t = Thread.currentThread();
try {
System.out.println(t.getName()+":正在执行B方法...");
Thread.sleep(5000);
System.out.println(t.getName()+":执行B方法完毕!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
总结
多线程
线程:单一的顺序执行流程就是一个线程,顺序执行:代码一句一句的先后执行。
多线程:多个线程并发执行。线程之间的代码是快速被CPU切换执行的,造成一种感官上"同时"执行的效果。
线程的创建方式
-
继承Thread,重写run方法,在run方法中定义线程要执行的任务
优点:
-
结构简单,便于匿名内部类创建
缺点:
-
继承冲突:由于java单继承,导致如果继承了线程就无法再继承其他类去复用方法
-
耦合问题:线程与任务耦合在一起,不利于线程的重用。
-
-
实现Runnable接口单独定义线程任务
优点:
-
犹豫是实现接口,没有继承冲突问题
-
线程与任务没有耦合关系,便于线程的重用
缺点:
-
创建复杂一些(其实也不能算缺点)
-
线程Thread类的常用方法
void run():线程本身有run方法,可以在第一种创建线程时重写该方法来定义线程任务。
void start():启动线程的方法。调用后线程被纳入到线程调度器中统一管理,并处于RUNNABLE状态,等待分配时间片开始并发运行。
注:线程第一次获取时间片开始执行时会自动执行run方法。 **启动线程一定是调用start方法,而不能调用run方法!**
String getName():获取线程名字
long getId():获取线程唯一标识
int getPriority():获取线程优先级,对应的是整数1-10
boolean isAlive():线程是否还活着
boolean isDaemon():是否为守护线程
boolean isInterrupted():是否被中断了
void setPriority(int priority):设置线程优先级,参数可以传入整数1-10。1为最低优先级,5为默认优先级,10为最高优先级
优先级越高的线程获取时间片的次数越多。可以使用Thread的常量MIN_PRIORITY,NORM_PRIORITY,MAX_PRIORITY。 他们分别表示最低,默认,最高优先级
static void sleep(long ms):静态方法sleep可以让运行该方法的线程阻塞参数ms指定的毫秒。
static Thread currentThread():获取运行该方法的线程。
void setDaemon(boolean on):设置线程是否为守护线程,当参数为true时当前线程被设置为守护线程。该操作必须在线程启动前进行
守护线程与普通线程的区别主要体现在当java进程中所有的普通线程都结束时进程会结束,在结束前会杀死所有还在运行的守护线程。
重点:多线程并发安全问题
-
什么是多线程并发安全问题:
当多个线程并发操作同一临界资源,由于线程切换时机不确定,导致执行顺序出现混乱。
解决办法:
将并发操作改为同步操作就可有效的解决多线程并发安全问题
-
同步与异步的概念:同步和异步都是说的多线程的执行方式。
多线程各自执行各自的就是异步执行,而多线程执行出现了先后顺序进行就是同步执行
-
synchronized的两种用法
1.直接在方法上声明,此时该方法称为同步方法,同步方法同时只能被一个线程执行
2.同步块,推荐使用。同步块可以更准确的控制需要同步执行的代码片段。
有效的缩小同步范围可以在保证并发安全的前提下提高并发效率
-
同步监视器对象的选取:
对于同步的成员方法而言,同步监视器对象不可指定,只能是this
对于同步的静态方法而言,同步监视器对象也不可指定,只能是类对象
对于同步块而言,需要自行指定同步监视器对象,选取原则:
1.必须是引用类型
2.多个需要同步执行该同步块的线程看到的该对象必须是同一个
-
互斥性
当使用多个synchronized修饰了多个代码片段,并且指定的同步监视器都是同一个对象时,这些代码片段就是互斥的,多个线程不能同时在这些代码片段上执行。