java线程详解(二)

1,线程安全

先看上一节程序,我们稍微改动一下:

//线程安全演示
//火车站有16张票,需要从四个窗口卖出,如果按照上面的多线程实现,程序如下
class Ticket implements Runnable
{
    private int tick = 16;//票的张数---16
    public void run(){
        while(true){
            if(tick>0){
                //这里的sleep(100)是这次程序要表的关键,只是个模拟而已
                try{
                    Thread.sleep(100);
                }catch(Exception e){

                }
              System.out.println(Thread.currentThread().getName() + "...sale:" + tick--);
            }
        }

    }
    public static void main(String[] args)
    {
        Ticket t = new 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();
    }
}

其他之处没有改变,只是在判断还有票之后立即睡眠一段时间,这在现实中有可能发生的,线程A刚执行到这里,突然cpu切换到其他线程B中,而B线程也刚好执行到判断语句又被A线程抢夺cpu,此时A线程不用再判断,直接输出,并修改ticket的值,此时有可能使得tickte>0不再满足,但是下一次B线程执行时,也不去判断tickte的值,导致出错。看看上面的程序结果(不为一),数了一下,打印了20张票,还有-1,-2,0的 情况,这就是线程安全问题了。

问题原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完另一个线程就来执行,导致共享数据的错误。

解决办法:对多条操作共享数据的语句,只能让一个线程都执行过程中,其他线程也不能执行。即使其他线程拿到执行权!这就是java提供的同步代码块方案。其格式如下:

synchronized(对象){

需要同步的代码块

}

分析:上面的synchronized后面的对象如同锁,也叫同步监视器,持有锁的线程可以同步执行,没有持有锁的线程即使获取cpu的执行权,也不能进不去同步代码块。线程开始执行同步代码块必须先对同步监视器的锁定。

同步前提:

(1)必须有两个或者两个以上的线程。

(2)必须是多个线程使用同一个锁。

任何时刻只有一个线程获得对同步监视器的锁定,当同步代码块执行结束时,该线程会释放对同步监视器的锁定。

如何看哪些代码需要同步?看看共享数据参与运算的范围。if(tick>0),还有tick—;因此把上面程序改为如下:

//线程安全演示
//火车站有16张票,需要从四个窗口卖出,如果按照上面的多线程实现,程序如下
class Ticket implements Runnable
{
    private int tick = 16;//票的张数---16
    Object obj = new Object();
    public void run(){
        while(true){
            synchronized(obj){
                if(tick>0){
                    //这里的sleep(100)是这次程序要表的关键,只是个模拟而已
                    try{
                        Thread.sleep(10);
                    }catch(Exception e){

                    }
                  System.out.println(Thread.currentThread().getName() + "...sale:" + tick--);
                }
            }
        }

    }
    public static void main(String[] args)
    {
        Ticket t = new 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();
    }
}

此时运行就没有出现错误。结果就不展示了。

好处:解决线程安全问题

缺点:多线程需要判断锁,较为消耗资源

2,线程同步

再看下面的例子

/*
需求:银行有一个金库,有两个储户分别存入300元,都是分3次存入
目的:查看该程序是否有安全问题,如果有,如何解决?
如何找问题:
1,明确哪些代码是多线程运行代码
2,明确哪些是共享数据
3,明确哪些代码使用共享数据
*/

class Bank
{
    //Object obj = new Object();  ------1
    private int sum;
    public synchronized void add(int n){ //synchronized可以放在此处作为同步函数
        //synchronized(obj){  ------2
            sum = sum + n;
            try{
                Thread.sleep(10);
            }catch(Exception e){

            }
            System.out.println("sum = " + sum);
        //} ------3
    }
}

class Cus implements Runnable
{
    private Bank b = new Bank();
    public void run(){
        for(int x = 0; x < 3; x++){
            b.add(100);
        }
    }
}

class BankDemo
{
    public static void main(String[] agrs){
        Cus c = new Cus();
        Thread t1 = new Thread(c);
        Thread t2 = new Thread(c);
        t1.start();
        t2.start();
    }

}

上面代码如果不同步就会出现线程安全问题,但是同步不止刚才那种同步代码块,还有同步函数可以使用,本程序就是个例子。但是对于卖票那个程序来试试

//线程安全演示
//火车站有16张票,需要从四个窗口卖出,如果按照上面的多线程实现,程序如下
class Ticket implements Runnable
{
    private int tick = 160;//票的张数---160
    //Object obj = new Object();
    public synchronized void run(){
        while(true){
            //synchronized(obj){
                if(tick>0){
                    //这里的sleep(1000)是这次程序要表的关键,只是个模拟而已
                    try{
                        Thread.sleep(100);
                    }catch(Exception e){

                    }
                  System.out.println(Thread.currentThread().getName() + "...sale:" + tick--);
                }
            //}
        }

    }
    public static void main(String[] args)
    {
        Ticket t = new 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();
    }
}

结果

其实这里面一直是线程0在执行,因为run方法是在同步,当0线程执行时别的线程都不能执行,这是因为同步的代码块范围不正确,只需要把需要同步的代码块封装为一个函数,便可以在run方法中调用这个函数即可。

//线程安全演示
//火车站有16张票,需要从四个窗口卖出,如果按照上面的多线程实现,程序如下
class Ticket implements Runnable
{
    private int tick = 160;//票的张数
    public void run(){
        while(true){
            show();//此处调用show方法
        }

    }

    public synchronized void show(){//将同步代码块封装起来
        if(tick>0){
            try{
                Thread.sleep(100);
            }catch(Exception e){

            }
             System.out.println(Thread.currentThread().getName() + "...sale:" + tick--);
        }
    }
    public static void main(String[] args)
    {
        Ticket t = new 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();
    }
}

此时可以看到几个线程都启动了。那么有个疑问,同步代码块这种方法有个同步监视器来进行锁住。那么同步方法怎么实现呢?

其实函数都需要对象调用,每个函数都有一个对象引用,就是隐含的this,所以同步函数使用的锁就是this。

通过程序来验证:

使用两个线程来实现卖票,第一个线程在同步函数中,第二个线程在同步代码块中。都在执行卖票动作。

class Ticket implements Runnable
{
    private int tick = 16;//票的张数
    Object obj = new Object();
    boolean flag = true;
    public  void run(){
        if(flag){
            while(true){
                synchronized(obj){
                    if(tick>0){
                        try{
                            Thread.sleep(100);
                        }catch(Exception e){

                        }
                      System.out.println(Thread.currentThread().getName() + "...code:" + tick--);
                    }
                }
            }
        }
        else{
            while(true){
                show();
            }
        }
    }

    public synchronized void show(){
        if(tick>0){
            try{
                Thread.sleep(100);
            }catch(Exception e){

            }
             System.out.println(Thread.currentThread().getName() + "...show:" + tick--);
        }
    }
    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(100);
        }catch(Exception e){

        }
        t.flag = false;
        t2.start();

    }
}

结果如下

先来解释一下,这个程序的run()方法中有同步代码块,还有一个同步函数。我们的意思是想让t1先执行,在main函数中让其sleep是因为如果没有sleep的话,主线程可能没有分配给t1的cpu执行权直接执行下面的flag = false这句,导致同步代码块中的代码永远不能执行。加入sleep可以执行同步代码块的代码,也可以执行同步函数中的代码,但是看到结果,打印了0号票,这是不允许的,是什么造成了这种情况?

看看文章最前面的同步的前提,有两个,再次提出:

(1)必须有两个或者两个以上的线程。-----满足

(2)必须是多个线程使用同一个锁。-----不满足-----〉第一个锁是synchronized(obj)中的obj,还有一个同步函数,它的锁是this,就是调用它的对象。

把synchronized(obj)这句换为synchronized(this),就不会出现上述问题,所以这样满足第二个前提,说明同步函数使用的的确是this这个锁。

有个情况,如果同步函数使用static修饰,那么静态方法中的锁是谁呢?肯定不是this,因为静态方法没有this。静态进内存时没有本类对象,但是有本类的字节码对象,该对象的类型是class.因此,对于静态方法,他的同步监视器(也就是锁)就是类名.class。把synchronized(obj)这句换为synchronized(Ticket.class),就不会出现0号票了。

3,死锁

死锁原因:同步中嵌套同步,但是锁却不同步。

先修改上面的程序

class Ticket implements Runnable
{
    private int tick = 1000;//票的张数
    Object obj = new Object();
    boolean flag = true;
    public  void run(){
        if(flag){
            while(true){
                synchronized(obj){
                    System.out.println("*****************");
                    show();
                }
            }
        }
        else{
            while(true){
                System.out.println("------------------");
                show();
            }
        }
    }

    public synchronized void show(){
        synchronized(obj){
            if(tick>0){
                try{Thread.sleep(10);}catch(Exception e){}
                 System.out.println(Thread.currentThread().getName() + "...code:" + tick--);
            }
        }
    }
    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(100);}catch(Exception e){}
        t.flag = false;
        t2.start();

    }
}

上面的程序出现死锁,原因是同步代码块中锁是obj,且嵌套同步函数(其锁是this),同步函数的锁是this,而其中又嵌套同步代码块,其锁是obj。可以修改锁即可。比如把obj全部换为this。这样代码就不会出错。但上述代码的输出-----和***之前加入一句判断语句if(tick>0)不然票卖完了还在打印。

上面的例子是同步代码块中包含同步函数,同步函数中又包含同步代码块,下面再看一个只在同步代码块中出现的死锁的例子。

class Test implements Runnable
{
    private boolean flag;
    Test(boolean flag){
        this.flag = flag;
    }
    public void run(){
        if(flag){
            synchronized(MyLock.locka){
                System.out.println("if a");
                synchronized(MyLock.lockb){
                    System.out.println("if b");
                }
            }
        }

        else{
            synchronized(MyLock.lockb){
                System.out.println("else b");
                synchronized(MyLock.locka){
                    System.out.println("else a");
                }
            }
        }
    }
}

class MyLock
{
    static Object locka = new Object();
    static Object lockb = new Object();
}

class DeadLockTest
{
    public static void main(String[] agrs){

        Thread t1 = new Thread(new Test(true));
        Thread t2 = new Thread(new Test(false));
        t1.start();
        t2.start();
    }
}

结合代码,在run()方法中,if语句中的同步代码块有嵌套,else也一样。他们也是这种情况:同步中嵌套同步,但是锁却不同步。

所以在以后的编程过程中一定要防止此类情况的发生。

注:这些代码和相关结论参见毕向东的java基础视频教程和李刚的《疯狂java讲义》,都非常不错!

时间: 2024-10-15 13:10:16

java线程详解(二)的相关文章

Java线程详解(二)

Java线程:新特征-线程池 线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理.当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源. 在使用线程池之前,必须知道如何去创建一个线程池,在Java5中,需要了解的是java.util.concurrent.Executors类的API,这个类提供大量创建连接池的静态方法,是必须掌握的. Java通过Executor

Java多线程详解(二)

评论区留下邮箱可获得<Java多线程设计模式详解> 转载请指明来源 1)后台线程 后台线程是为其他线程服务的一种线程,像JVM的垃圾回收线程就是一种后台线程.后台线程总是等到非后台线程死亡之后,后台线程没有了服务对象,不久就会自动死亡,不再复活.利用setDaemon方法可以把一个线程设置为后台线程,但必须在线程启动之前调用. 例如 : /* * @author [email protected] */ public class DaemonThread extends Thread { pu

java线程详解

Java线程:概念与原理 一.操作系统中线程和进程的概念 现在的操作系统是多任务操作系统.多线程是实现多任务的一种方式. 进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程.比如在Windows系统中,一个运行的exe就是一个进程. 线程是指进程中的一个执行流程,一个进程中可以运行多个线程.比如java.exe进程中可以运行很多线程.线程总是属于某个进程,进程中的多个线程共享进程的内存. “同时”执行是人的感觉,在线程之间实际上轮换执行. 二.Jav

spark2.x由浅入深深到底系列六之RDD java api详解二

package com.twq.javaapi.java7; import org.apache.spark.SparkConf; import org.apache.spark.api.java.JavaRDD; import org.apache.spark.api.java.JavaSparkContext; import org.apache.spark.api.java.function.Function2; import org.apache.spark.api.java.funct

Java线程详解(三)

Java线程:新特征-有返回值的线程 在Java5之前,线程是没有返回值的,常常为了"有"返回值,破费周折,而且代码很不好写.或者干脆绕过这道坎,走别的路了. 现在Java终于有可返回值的任务(也可以叫做线程)了. 可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口. 执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了. 下面是个很简单的例子: import jav

Java线程详解(一)

程序.进程.线程的概念  程序(program):是为完成特定任务.用某种语言编写的一组指令的集合.即指一段静态的代码,静态对象.  进程(process):是程序的一次执行过程,或是正在运行的一个程序.动态过程:有它自身的产生.存在和消亡的过程.     如:运行中的QQ,运行中的MP3播放器     程序是静态的,进程是动态的  线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径.     若一个程序可同一时间执行多个线程,就是支持多线程的 Java中多线程的创建和使

Java线程详解----借鉴

Java线程:概念与原理 一.操作系统中线程和进程的概念 现在的操作系统是多任务操作系统.多线程是实现多任务的一种方式. 进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程.比如在Windows系统中,一个运行的exe就是一个进程. 线程是指进程中的一个执行流程,一个进程中可以运行多个线程.比如java.exe进程中可以运行很多线程.线程总是属于某个进程,进程中的多个线程共享进程的内存. “同时”执行是人的感觉,在线程之间实际上轮换执行. 二.Jav

java 线程详解

5月7号  周末看了一下线程方面的内容 ,边看视频边看书还附带着参考了很多人的博客,一天的收获,写下来整理一下:感觉收获还是挺多的:过段时间可能看完java  这几大块要去看一下关于spring boot  的内容顺便  也整理一下:附上我参考的 几本书: 关于java  线程,首先要了解一下线程和进程之间的关系.区别以及他们之间的概念: 首先是线程: 什么是线程? 线程是在程序执行过程中能够执行部分代码的一个执行单元,也看看做是一个轻量级的进程:线程是程序内的程序控制流只能使用程序内分配给程序

java线程详解(三)

java线程间通信 首先看一段代码 class Res { String name; String sex; } class Input implements Runnable { private Res r; Input(Res r) { this.r = r; } public void run() { int x = 0; while(true){ if(x==0){ r.name = "mike"; r.sex = "male"; } else{ r.nam

并发编程 || Java线程详解

通用线程模型 在很多研发当中,实际应用是基于一个理论再进行优化的.所以,在了解JVM规范中的Java线程的生命周期之前,我们可以先了解通用的线程生命周期,这有助于我们后续对JVM线程生命周期的理解. 首先,通用的线程生命周期有五种,分别是:新建状态(NEW).可运行状态(RUNNABLE).运行状态(RUN).休眠状态(SLEEP).终止状态(TERMINATED).生命流程如下图所示: 新建状态(NEW).线程在此状态,仅仅是在编程语言层面创建了此线程,而在真正的操作系统中是没有创建的.所以,