Java多线程 ----(2)多线程同步

1 同步函数
2 同步代码块
3 锁

另外,这篇文章尝试了一种新的行文风格,轻喷。

1、多线程存在的一个问题:当多个线程存在时,可能会出现同步问题。

例如,在某一个线程休眠的时候,可能其他线程会进入if语句继续执行,而当这个线程苏醒继续执行剩余的代码,程序的状态已经发生巨大变化,继续执行就会导致无法预计的后果。
例如,常见的卖票程序,总共100张票,在某个线程休眠之后,后进来的接着执行,会出现-1,-2号票。

//创建Ticket类,实现Runnable方法
public class Ticket implements Runnable{

    private int ticketCount = 100;
    public void run(){
        while(true){
            if(ticketCount>0){//该语句未生效,结果中出现了0,-1,-2号票
                 try{
                   Thread.sleep(10);//先进来的线程休眠,后进来的接着执行,可能出现异常
                }catch(InterruptException){
                    e.printStack();
                }
                System.out.println(Thread.currentThread().getName()+"------"+ticketCount--);
            }
        }
    }
}

public static void main(String args[]){
        Ticket t = new Ticket();
        //启动四个线程,共享Ticket
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        Thread t4 = new Thread(t);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
}
image.png

2、解决这个问题

常见的方法是使用Java提供的同步代码块,语法如下:

synchronized(对象){
}

修改后代码如下:

//创建Ticket类,实现Runnable方法
public class Ticket implements Runnable{

    private int ticketCount = 100;
    Object obj = new Object();//必须有对象,代表锁,必须放在执行代码的外层,保证其唯一性
    public void run(){
        while(true){
          synchronized(obj){
            if(ticketCount>0){
                 try{
                   Thread.sleep(10);//可能出现异常
                }catch(InterruptException){
                    e.printStack();
                }
                System.out.println(Thread.currentThread().getName()+"------"+ticketCount--);
            }
          }
        }
    }
}

通过上述的锁即可解决多线程的安全问题。

对象就如同锁。持有锁的线程可以在同步中执行。
没有持有锁的线程即使获取到了CPU的执行权,也无法进入上锁的代码块。

同步的前提:
1、必须要有两个或者两个以上的线程;
2、必须是多个线程同时使用一个锁;
同步的好处:解决了多线程安全问题。
弊端:多个线程需要判断,较为消耗资源。

3、同步函数

除了同步代码块之外,也可以用同步函数来来实现代码同步的功能。
例如:

//创建Ticket类,实现Runnable方法
public class Ticket implements Runnable{
    private int ticketCount = 100;
    public synchronized void run(){
        while(true){
            if(ticketCount>0){
                 try{
                   Thread.sleep(10);//可能出现异常
                }catch(InterruptException){
                    e.printStack();
                }
                System.out.println(Thread.currentThread().getName()+"------"+ticketCount--);
            }
          }
    }
}

但是,上述代码中同步函数的使用会导致一个问题,如图所示:

image.png

原因是:在0线程未执行完毕之前,其他的线程无法获得执行权,进入到该方法中来。

所以,在使用同步代码时:
必须要搞清楚“哪些些代码需要被同步,哪些则不能被同步!”

那么在改方法中需要同步的代码只涉及,ticket的操作,故可以将该部分代码抽取出来,使用一个同步函数表示,如下:

private int ticketCount = 100;
    public void run(){
        while(true){
            show();
        }
    }

   //抽取出需要同步的代码
    public synchronized void show(){
        if(ticketCount>0){
            try{
                Thread.sleep(10);
            }catch(Exception e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"**********"+ticketCount--);
        }
    }

如此,即可实现多线程安全的功能


image.png

4 、新的问题:在上述代码中,似乎并没有新创建一个锁对象

Object obj = new Object();//被删除了

那么,在这个时候,同步函数使用的锁是哪个呢?

结论:同步函数使用的是this锁。

验证:
写一个小程序来测试一下,如下:
功能:声明两种加锁的方式来实现同步,一种是函数块,一种是代码块;

假设1:如果说两种方式使用的是同一个锁。如果是这样,下面的程序应该可以很好的完成同步功能,不会出现异常。

代码如下:

public class Ticket implements Runnable{

    private int ticketCount = 100;
    Object obj = new Object();
    boolean flag = true;
    public void run(){
        if(flag){
            while(true){
                show();
            }
        }else{
            while(true){
                synchronized (obj) {
                    if(ticketCount>0){
                        try{
                            Thread.sleep(10);
                        }catch(Exception e){
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()+"*****NOT-SHOW*****"+ticketCount--);
                    }
                }
            }
        }
    }

    public synchronized void show(){
        if(ticketCount>0){
            try{
                Thread.sleep(10);
            }catch(Exception e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"****SHOW******"+ticketCount--);
        }
    }
}

主函数如下:

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(10);//必须让主线程休眠一会儿,否则flag总是false
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.flag = false;
        t2.start();
    }

运行结果如下


image.png

通过上图中0号票的存在,
假设1被推翻,即两种同步代码用的并不是同一个锁。

假设2:show()同步函数使用的是this锁,代码块使用的是new的Object锁(已知)。

验证代码如下:将obj换成this,若未出现安全问题,则说明show()使用的是this锁。

public class Ticket implements Runnable{

    private int ticketCount = 100;
    Object obj = new Object();
    boolean flag = true;
    public void run(){
        if(flag){
            while(true){
                show();
            }
        }else{
            while(true){
                synchronized (this) {
                    if(ticketCount>0){
                        try{
                            Thread.sleep(10);
                        }catch(Exception e){
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()+"*****NOT-SHOW*****"+ticketCount--);
                    }
                }
            }
        }
    }

    public synchronized void show(){
        if(ticketCount>0){
            try{
                Thread.sleep(10);
            }catch(Exception e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"****SHOW******"+ticketCount--);
        }
    }
}

运行结果如下:


image.png

结果中并未出现0号异常票,所以,假设得证!

结论:同步函数使用的是this锁。

5 再说一个要注意的地方:

如果同步函数被静态所修饰,使用的锁是该方法所在类的字节码文件对象,可通过类名称.class获得。
代码例子:

public class Ticket implements Runnable{
private static int ticketCount = 100;
    Object obj = new Object();
    boolean flag = true;
    public void run(){
        if(flag){
            while(true){
                show();
            }
        }else{
            while(true){
                synchronized (Ticket.class) {
                    if(ticketCount>0){
                        try{
                            Thread.sleep(10);
                        }catch(Exception e){
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()+"*****NOT-SHOW*****"+ticketCount--);
                    }
                }
            }
        }
    }

    public static synchronized void show(){
        if(ticketCount>0){
            try{
                Thread.sleep(10);
            }catch(Exception e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"****SHOW******"+ticketCount--);
        }
    }
}

结果如下:


image.png

结果正常,无0号票,但是如果不使用Ticket.class,使用this就会出现0号票。

END


P.S.第一次采用这种行文方式来写,感觉不错,更加面向浏览者,是个好的转变!O(∩_∩)O哈哈~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。