多线程【线程的安全问题及synchronized同步锁】
1、线程的安全问题
1.1、线程安全问题的发生
上述买票的例子,继续进行分析发现,当四个线程都开启之后,CPU会在这四个线程之间随机切换。
class ThreadDemo1 {
public static void main(String[] args) {
//1,创建Runnable接口的子类对象。
Ticket t = new Ticket();
//2,创建四个线程对象。并将Runnable接口的子类对象作为参数传递给Thread的构造函数。
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//3,开启四个线程。
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Ticket implements Runnable {
//1、描述票的数量。
private int tickets = 100;
//2、售票的动作,这个动作需要被多线程执行,那就是线程任务代码。需要定义run方法中。
//线程任务中通常都有循环结构。
public void run() {
while(true) {
if(tickets>0) { //由于run方法是复写接口中的,run方法没有抛出异常, 此时这里只能捕获异常,而不能抛出
//让线程在此冻结10毫秒
try{
Thread.sleep(10);
}catch(InterruptedException e)
{
/*异常处理代码*/
}
System.out.println(Thread.currentThread().getName()+"....."+tickets--);//打印线程名称。
}
}
}
}
当四个线程把票卖的只剩下最后一张时,发生了上图的情况,这时会发现输出的票出现0号票、-1号票和-2号票。对于多线程操作最怕的就是出现线程安全问题。
1.2、线程安全问题发生的原因
对上述代码进行图解说明发现,得出发生问题的原因。
问题产生的原因:
- 线程任务中在操作共享的数据。
- 线程任务操作共享数据的代码有多条(运算有多个)。
解决共享的资源:
只要让一个线程在执行线程任务时将多条操作共享数据的代码执行完,在执行过程中,不要让其他线程参与运算。就可以解决这个问题。而要解决这个问题,就要用到我们的下一个知识:同步(synchronized)。
2、synchronized同步锁
解决上述的问题Java中给我们提供相应的独立代码块,这段代码块需要使用关键字synchronized
来标识其为一个同步代码块。
synchronized
关键字在使用时需要个对象作为标记,当任何线程进入synchronized标识的这段代码时,首先都会先判断目前有没有线程正在使用synchronized标记对象,若有线程正在使用这个标记对象, 那么当前这个线程就在synchronized标识的外面等待,直到获取到这个标记对象后,这个线程才能执行同步代码块。
public class ThreadDemo2 {
public static void main(String[] args) {
//1,创建Runnable接口的子类对象。
Ticket t = new Ticket();
//2,创建四个线程对象。并将Runnable接口的子类对象作为参数传递给Thread的构造函数。
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//3,开启四个线程。
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Ticket1 implements Runnable {
//1、描述票的数量。
private int tickets = 100;
//2、售票的动作,这个动作需要被多线程执行,那就是线程任务代码。需要定义run方法中。
//定义同步代码块的标记对象。相当与锁的功能
private Object obj = new Object();
public void run() {
//线程任务中通常都有循环结构。
while(true) {
//使用同步代码块解决线程安全问题。
synchronized(obj) {
if(tickets>0) { //由于run方法是复写接口中的,run方法没有抛出异常, 此时这里只能捕获异常,而不能抛出
//让线程在此冻结10毫秒
try{
Thread.sleep(20);
}catch(InterruptedException e){
/*异常处理代码*/
System.out.println(e);
}
System.out.println(Thread.currentThread().getName()+"....."+tickets--);//打印线程名称。
}
}
}
}
}
2.1、同步的好处与弊端:
同步好处:解决多线程安全问题。这里举例(火车上的卫生间)说明同步锁机制。
同步弊端:降低了程序的性能。每个线程都要去判断锁机制,那么会增加程序运行的负担,同时只要做判断,CPU都要处理,那么也会消耗CPU的资源。即就是加同步会降低程序
的性能。
2.2、同步的前提:必须保证多个线程在同步中使用的是同一个锁。
测试如下代码:
与上述代码区别:
// 之前的
private Object obj = new Object();
synchronized(obj) ;
// 现在的
synchronized(new Object());
public class ThreadDemo2 {
public static void main(String[] args) {
//1,创建Runnable接口的子类对象。
Ticket t = new Ticket();
//2,创建四个线程对象。并将Runnable接口的子类对象作为参数传递给Thread的构造函数。
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//3,开启四个线程。
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Ticket2 implements Runnable
{
//1、描述票的数量。
private int tickets = 100;
//2、售票的动作,这个动作需要被多线程执行,那就是线程任务代码。需要定义run方法中。
//定义同步代码块的标记对象。相当与锁的功能
//private Object obj = new Object();
public void run()
{
//线程任务中通常都有循环结构。
while(true)
{
//使用同步代码块解决线程安全问题。
synchronized(new Object())
{
if(tickets>0)
{ //由于run方法是复写接口中的,run方法没有抛出异常, 此时这里只能捕获异常,而不能抛出
//让线程在此冻结10毫秒
try{Thread.sleep(10);}catch(InterruptedException e){/*异常处理代码*/}
System.out.println(Thread.currentThread().getName()+"....."+tickets--);//打印线程名称。
}
}
}
}
}
多个线程操作了共享数据,并且操作共享数据的代码有多句,必须使用同步代码块来解决,当线程任务代码只会有一个线程执行时,加不加同步都可以。当线程任务代码会有被多个线程执行时,这时需要加同步,但是加同步时一定要保证多个线程使用的是同一把锁。上述代码发生的安全问题就是因为每个线程都有自己的Object对象作为自己的锁。
注意:
当多线程安全问题发生时,加入了同步后,问题依旧,就要通过这个同步的前提来判断同步是否写正确。
2.3、同步函数锁
卖票的例子改写封装成sale()函数来实现:
class ThreadDemo3 {
public static void main(String[] args) {
//1,创建Runnable接口的子类对象。
Ticket t = new Ticket();
//2,创建四个线程对象。并将Runnable接口的子类对象作为参数传递给Thread的构造函数。
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//3,开启四个线程。
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Ticket implements Runnable{
//1、描述票的数量。
private int tickets = 100;
//2、售票的动作,这个动作需要被多线程执行,那就是线程任务代码。需要定义run方法中。
//定义同步代码块的标记对象。相当与锁的功能
private Object obj = new Object();
public void run(){
//线程任务中通常都有循环结构。
while(true){
//调用sale函数完成卖票过程
sale();
}
}
//将售票的代码封装到sale函数中
public void sale(){
if(tickets>0){
//由于run方法是复写接口中的,run方法没有抛出异常, 此时这里只能捕获异常,而不能抛出
//让线程在此冻结10毫秒
try{
Thread.sleep(10);
}catch(InterruptedException e){
/*异常处理代码*/
}
//打印线程名称。
System.out.println(Thread.currentThread().getName()+"....."+tickets--);
}
}
}
方法上加的同步,使用的同步锁是:
代码进行改造进行测试,让一个线程在同步代码块中执行,一个线程在同步函数中执行,修改同步代码块中的锁,测试会不会出现线程安全问题。
class Ticket implements Runnable {
private int tickets = 100;
private Object obj = new Object();
boolean flag = true;
public void run() {
//通过标记实现线程切换,让一个线程在同步函数中执行,一个线程在同步代码块中执行
if(flag) {
//线程任务中通常都有循环结构。
while(true) {
synchronized(obj) {
if(tickets>0) {
//由于run方法是复写接口中的,run方法没有抛出异常, 此时这里只能捕获异常,而不能抛出
//让线程在此冻结10毫秒
try{
Thread.sleep(10);
}catch(InterruptedException e){
System.out.println(e);
}
System.out.println(Thread.currentThread().getName()+"..obj..."+tickets--);
}
}
}
}
else {
while(true) {
//调用sale函数完成卖票过程
sale();
}
}
}
//将售票的代码封装到sale函数中
public synchronized void sale() {
if(tickets>0) {
try{
Thread.sleep(10);
}catch(InterruptedException e){
System.out.println(e);
}
System.out.println(Thread.currentThread().getName()+"..sale..."+tickets--);
}
}
}
public class ThreadDemo4 {
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try{Thread.sleep(1);}catch(InterruptedException e){}
//修改标记,实现线程执行代码的切换,在切换之前需要让主线程短暂停顿,保证两个线程可以切换
t.flag = false;
t2.start();
}
}
发现打印的数据还是出现了问题,说明同步函数和同步代码块他们使用的不是同一把锁。
函数是需要对象去调用的,那么同步函数中使用的锁会不会就是当前调用这个方法的对象呢?把同步代码块中的锁改为this
后发现问题解决了,从而得出原来同步函数中使用的锁为this
锁。
2.4、同步代码块使用的锁
同步代码块使用的锁可以是任意对象的。因为synchronized中的对象可以我们自己指定。
2.5、同步代码块和同步函数的区别
同步函数使用的锁是固定的this。当线程任务只需要一个同步时完全可以使用同步函数。
同步代码块使用的锁可以是任意对象。当线程任务中需要多个同步时,必须通过锁来区分,这时必须使用同步代码块。同步代码块较为常用。
2.6、静态同步函数使用的锁
静态是可以修饰成员的,那么sale方法也是可以被静态修饰的。如果这个方法被静态修饰了会怎么样。将sale方法改为静态后,发现安全问题再次发生了。其实静态同步函数使用的锁是字节码class文件对象。把同步代码块中的锁改为Ticket.class后
,问题解决了。静态同步函数中使用的锁为class文件对象。
总结:static 同步函数,使用的锁不是this,而是字节码文件对象, 类名.class