黑马程序员_Java基础_线程基础,创建,同步(单例设计模式的同步),死锁

来源:互联网 发布:linux清除所有arp缓存 编辑:程序博客网 时间:2024/06/01 10:29

 一,进程与线程

1,进程定义:进程就是指正在执行的程序,怎样查看正在执行的进程呢?我们在使用电脑的时候,其实就有多个正在执行的程序,通过Ctri+Alt+Del 组合键可以进入windows任务管理器查看进程,我们进入后会看到很多.exe,这些就是我们的电脑当前正在执行的程序,也就是一个个的进程。

每一个程序执行的都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元。

 

2,线程:是进程中一个独立控制单元,控制着进程的执行。一个进程中至少有一个线程。我们之前写的程序都是单线程的程序。我们在编译java文件的时候,会启动javac进程,启动java命令时,会启动JVM执行.class文件。这个进程中至少有一个负责线程的执行,这个线程运行的代码就是main函数里面的代码,该线程称之为主线程。

 

注意:通常我们可以理解为这样的程序时单线程程序,实际上并不止就这一个线程。原因是还有一个线程就是JVM的垃圾回收机制中控制垃圾回收的线程,在主线程执行的时候会启动,回收内存中不再使用的对象的内存,其实最少有两个线程。

 

多线程最常见的应用就是下载软件,下载软件在下载东西的时候,就是多个线程同时向服务器发送请求,同时多条路劲在下载文件。其实多线程在执行的时候并不是多个线程同时执行,而是CPU在多个线程之间进行这快速的切换,中间的事件间隔我们可以忽略,因为太快了,所以我们认为是同时在执行,其实这种执行叫做并发执行。

 

二,线程的五种状态:


(1)被创建:创建Thread类的子类,将要运行的代码放在run方法中,调用start方法创建线程,并调用run方法,此时线程进入运行状态。

(2)运行:运行状态就是run方法中的代码执行过程,调用stop方法终止整个线程,run方法结束。运行状态时调用sleep方法或者wait方法,是线程进入(3)冻结状态,此时线程放弃了执行权,当睡眠时间或者从冻结状态调用notify方法,能从冻结状态转化为运行状态。

(4)阻塞:这个状态比较特殊,这个状态线程具有执行权,但是在等待CPU资源,这个状态有可能在run中的代码运行一部分还没运行完时,CPU去执行其他线程中的代码去了。冻结状态被叫醒后不一定直接进入运行状态,也有可能进入阻塞状态。当然阻塞状态也有可能进入冻结状态。冻结状态:没有了执行权,当然某个线程睡眠或者等待的时候。

(5)消亡:也就是该线程结束,run中的代码执行完。如果中途要关闭,则通过调用stop方法,否则线程执行完自动消亡。

 

三,自定义线程:

参考java文档的Thread类时,发现:

创建新执行线程有两种方法。一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例。例如,计算大于某一规定值的质数的线程可以写成:

本地图片,请重新上传

     class PrimeThread extends Thread {

         long minPrime;

         PrimeThread(long minPrime) {

             this.minPrime = minPrime;

         }

 

         public void run() {

             // compute primes larger than minPrime

              . . .

         }

     }

然后,下列代码会创建并启动一个线程:

     PrimeThread p = new PrimeThread(143);

     p.start();

通过API的解释可以看出创建一个线程的方式一继承Thread类:

1,创建一个类,继承Thread

2,重写Thread类中的run方法

3,调用线程的启动方法,start,该方法的作用有两个,一个是启动线程和调用run方法。

示例一: 

class RunDemo extends Thread {    public void run() {        for(int i=0;i<70;i++) {            System.out.println("Demo run ......");        }    }}public class ThreadDemo {    public static void main(String[] args) {        RunDemo d = new RunDemo();        d.start();                for(int i=0;i<70;i++) {            System.out.println("main run...");        }    }}

个程序在执行的时候应该是Demo runmain run是交替执行的。执行过程是主线程启动,main函数执行,然后RunDemo线程启动,run方法和main方法里面的for循环并发执行。

4,为什么定义一个继承Thread类的类的线程时候,要调用start方法,而不调用run方法呢?原因是如果调用了run方法,那么就不是一个独立的控制单元控制一段代码块的执行了,就成了方法的调用,程序中相当于只有一个main线程。程序会按顺序执行。

 

示例二:定义一个线程类,然后在main方法中启动两个自定义线程,交替执行线程中的内容。

class RunDemo extends Thread {    //private String name;    RunDemo(String name) {        //this.name = name;        super(name);//调用父类的构造函数给自定义线程赋一个名字    }    public void run() {        for(int i=0;i<70;i++) {            System.out.println(Thread.currentThread().getName() + " run ......" + i);            //System.out.println(this.getName() + " run ......" + i);等价于上面这个        }    }}public class ThreadDemo {    public static void main(String[] args) {        RunDemo d = new RunDemo("one");        RunDemo d1 = new RunDemo("two");        d.start();        d1.start();        /*for(int i=0;i<70;i++) {            System.out.println("main run...");        }*/    }}


注意:每个线程都有默认的名字,Thread-编号,编号是从0开始的。

Thread.currentThread().getName()可以获得线程对象的名字,通过setName或者构造函数可以设置名字,其他操作查看java文档。

 

自定义线程方式二:

创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。采用这种风格的同一个例子如下所示:

本地图片,请重新上传

     class PrimeRun implements Runnable {

         long minPrime;

         PrimeRun(long minPrime) {

             this.minPrime = minPrime;

         }

 

         public void run() {

             // compute primes larger than minPrime

              . . .

         }

     }

然后,下列代码会创建并启动一个线程:

     PrimeRun p = new PrimeRun(143);

     new Thread(p).start();

实现Runnable接口的方式创建线程步骤:

1,定义一个类实现Runnable接口;

2,覆盖Runnable接口中的run方法;目的:将线程要运行的代码存放在该run方法中;

3,通过Thread类建立线程对象;

4,将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数;原因是:run方法是属于Runnable接口的子类对象,所以要让线程指定所指定对象的run方法,就必须明确run方法所属的对象;

5,调用Thread类的run方法,启动线程,并调用Runnable接口子类的run方法。

 

示例:

    需求:模拟火车站窗口购票系统,一共100张票

    分析:利用多线程的原理,火车票多个窗口同时在卖一定数量的火车票,当窗口1卖了1号座位的车票,其他窗口就不能再卖1号座位的票了;那个窗口卖的是几号座位的票取决于cpu的执行顺序,多个窗口卖火车票,相当于多个线程在同时执行;如果使用方式一创建线程必定出问题,假设四个窗口,每个窗口都要创建一个Thread子类的对象,这样一共就是400张票。如果只创建一个对象,让该对象运行四次,那么运行时必定会出现错误提示,线程状态错误。所以使用这种方式创建时不可以的。解决方法是使用第二种创建线程的方式:

代码如下:

class Demon2 implements Runnable {    private int tickets = 100;    Object obj = new Object();    public void run() {        while(true) {            if(tickets>0) {                System.out.println(Thread.currentThread() + "print ticket no:" + tickets --);            }        }    }}class ThreadTest2 {    public static void main(String[] args) {        //创建Runnable接口子类对象        Demon2 d = new Demon2();        //创建线程对象,将Runnable接口的子类对象传给线程        Thread t1 = new Thread(d);        Thread t2 = new Thread(d);        Thread t3 = new Thread(d);        Thread t4 = new Thread(d);        //启动线程        t1.start();        t2.start();        t3.start();        t4.start();    }}

总结:实现方式和继承方式的区别:

继承Thread,线程代码存放在Tread子类的的run方法中。

实现Runnable,线程代码存放在接口的子类的run方法中。

实现的好处:避免了单线程的局限性。定义线程的时候,第一种方式不建议使用。建议使用第二种方式。

,线程的同步:

1,上述卖票系统,在判断tickets之后让该线程睡眠1秒钟,这时候通过分析发现会打印出0-1-2-3等错误座位,这就是多线程存在的安全问题。原因:当多条语句在操作同一个线程共享数据时,一个线程的多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误;

 

解决方法:

对多条操作共享数据的语句,只能让一个线程执行完,在执行过程中,其他线程不可以参与进来执行;

2Java对多线程的安全问题有专门的解决方法:就是同步代码块;

synchronized(对象) {

需要被同步的代码块

}

对象如同锁,持有锁的线程可以在同步中执行。没有持有锁的线程即使获得cpu的执行权,也进不去,因为没有锁;哪些代码需要同步,就看哪些语句在操作共享数据;

 

3,同步的前提是:

(1)要有两个或两个以上的线程;(2)必须是多个线程使用同一个锁;(3)必须保证同步中只能有一个线程在执行;

请看下面示例:

class Demon2 implements Runnable {    private int tickets = 100;    Object obj = new Object();    public void run() {        while(true) {            synchronized(obj) {  //重点部分,加锁了。每个线程进来之前都会判断该锁是否开启,                                //如果开启就进入,然后将锁关闭,这样后来的线程就没法进入,等之前进来的程序执行完后才能进来                if(tickets > 0) {                    try{Thread.sleep(10);}catch(Exception e) {}                    System.out.println(Thread.currentThread() + "print ticket no:" + tickets --);                }            }        }    }}class ThreadTest2 {    public static void main(String[] args) {        Demon2 d = new Demon2();        Thread t1 = new Thread(d);        Thread t2 = new Thread(d);        t1.start();        t2.start();    }}

4,在函数上加锁:

    虽然锁的方法有两种,就是锁住共享数据部分或者锁住函数,但是这里如果直接给函数上锁的话,一旦一个线程进去之后就不能出来,所以不能直接在run函数上加锁,要先将共享数据封装到一个函数内部,然后多该封装函数加锁。


class Demon2 implements Runnable {    private int tickets = 1000;    //Object obj = new Object();    boolean flag = true;    public void run() {        if(flag) {            while(true) {                synchronized(this) {  //如果改成自定义的obj,那么这两个进程使用的锁就不是同一个锁,不满足同步的条件                    if(tickets > 0) {                        try{Thread.sleep(10);}catch(Exception e) {}                        System.out.println(Thread.currentThread() + "print ticket no:" + tickets --);                    }                }            }        }        else            while(true){                show();            }    }    public synchronized void show() {//这里的锁使用的对象时this        if(tickets > 0) {            try{Thread.sleep(10);}catch(Exception e) {}            System.out.println(Thread.currentThread() + "print ticket no:" + tickets --);        }    }}class ThreadTest2 {    public static void main(String[] args) {        Demon2 d = new Demon2();        Thread t1 = new Thread(d);        Thread t2 = new Thread(d);        t1.start();t1.flag = flase;        t2.start();    }}
上面的程序,如果将show方法改成静态,那么所的对象就是类对象的字节码,Demo2.classsynchronized(this)要将this改成Demo2.class程序才能正常运行.原因是静态在类一加载就进内存了,类进内存的时候是没有类的,但一定有该类对应的字节码文件,他也是一个对象。所以静态同步使用的锁是该方法所在类的字节码文件对象。类名.class
 

五,结合线程同步的单例设计模式。


//饿汉式class Single {    private static final Single s = new Single();    private Single() {}    public static Single getInstance() {        return s;    }}//带延迟加载的懒汉式class Single2 {    private static Single2 s = null;    private Single2() {}    public static Single2 getInstance() {        if(s == null) {            synchronized (Single2.class) {                if(s == null) {                    s = new Single2();                }            }        }        return s;    }}

重点:面试中可能会问到:懒汉式和饿汉式的区别是什么?回答:懒汉式的特点在于延迟加载。懒汉式的延迟加载有没有什么问题?回答:如果是多线程访问时会出现安全问题。解决方法是同步来解决。用同步代码块和同步函数都可以,但是效率比较低。用双重判断的方式能够解决效率问题。同步的时候的锁是属于该类所属的字节码文件对象。

 

六,死锁。

所谓的死锁就是指,两个进程各自拿着各自的锁而不是放资源,而每个线程要想运行,就必须拿到对方的锁,这时候就会出现死锁的问题。

关于死锁的示例:


class ThreadDead implements Runnable {    private boolean flag;    public ThreadDead(boolean flag) {        this.flag = flag;    }    public void run() {        while(true) {            if(flag) {                synchronized (MyLock.lock1) {                    System.out.println("if lock1 run...");                    synchronized (MyLock.lock2) {                        System.out.println("if lock2 run...");                    }                }            }            else {                synchronized (MyLock.lock2) {                    System.out.println("else lock2 run...");                    synchronized (MyLock.lock1) {                        System.out.println("else lock1 run...");                    }                }            }        }    }}class MyLock {    static Object lock1 = new Object();    static Object lock2 = new Object();}public class DeadLock {    public static void main(String[] args) {        Thread t1 = new Thread(new ThreadDead(true));        Thread t2 = new Thread(new ThreadDead(false));        t1.start();        t2.start();    }}





0 0
原创粉丝点击