多线程问题

来源:互联网 发布:手机qq java 编辑:程序博客网 时间:2024/05/23 02:04

1、线程问题:

1.1实现多线程的方式:

方式一:继承Thread

1)为什么要继承Thread类?

在Java中使用Thread这个类对线程进行描述。而我们现在希望通过自己的代码操作线程,自己的代码应该需要和Thread类之间产生关系。这里我们采用的继承的关系。

当我们继承了Thread类之后,我们自己的类也就变成了线程类。我们自己的类就继承到了Thread类中的所有功能,就具备了操作线程的各种方法,并且自己的类就可以对线程进行各种操作(开启线程等)。

而我们自己定义的类称为了一个线程类,主要是因为可以复写run方法。

2)为什么要复写run方法?

为什么要使用线程:因为我们希望程序中的某段代码可以同时运行,提高程序的运行效率。

我们定义的类继承了Thread类之后,其实在Thread类中有个run方法,它是开启线程之后,就会直接去运行的方法。而Java在设计线程类(Thread)的时候,就已经明确了线程应该执行的某段代码需要书写在run方法中,也就是说在run方法中的代码开启线程之后才能正常的运行。

 

我们使用线程的目的是让线程执行后来自己程序中的某些代码,而Java中规定需要线程执行的代码必须写run方法中,Thread类中的run方法中并没有我们真正需要多线程运行的代码,而开启线程又要去运行run方法,这时我们只能沿用Thread类run方法的定义格式,然后复写run方法的方法体代码。

  简单来讲:

         设计Thread这个API的人,在设计的时候,只设计了如何启动线程,至于线程要执行什么任务,他并不知道。所以,他这样设计:就是start启动线程之后,JVM会自动的调用run方法。

因此,我们只要把自己的代码写到run方法中,就一定会被执行到。

 

3)为什么要调用start而不是run?

当书写了一个类继承了Thread类之后,这个子类也变成线程类。这时可以创建这个子类的对象,一旦创建Thread的子类对象,就相当于拥有了当前的线程对象。

 

创建Thread的子类对象,只是在内存中有了线程这个对象,但是线程还不能真正的去运行。

要让线程真正的在内存运行起来,必须调用start方法,因为start方法会先调用系统资源,启动线程。这样才能够在内存开启一片新的内存空间,然后负责当前线程需要执行的任务。

我们直接通过线程对象去调用run方法,这时只是对象调用普通的方法,并没有调用系统资源,启动线程,也没有在内存中开启一个新的独立的内存空间运行任务代码。只有调用start方法才会开启一个独立的新的空间。并在新的空间中自动去运行run方法。

  注意:run方法仅仅是封装了线程的任务。它无法启动线程。

         run:只是封装线程任务。

         start:先调用系统资源,在内存中开辟一个新的空间启动线程,再执行run方法。

 

4)同一个线程对象是否可以多次启动线程?

不能。如果同一个对象多次启动线程就会报如下异常。

代码如下所示:

/*

 需求:演示:获取和设置线程的名称。

 获取线程的名字使用函数getName()

 线程的默认名字是:Thread-x x是从0开始的递增数字

 */

//定义一个类继承Thread,这个类称为线程类

class MyThread2extends Thread {

    // B:复写Thread类中的run函数

    publicvoid run() {

        // 这里就是线程要执行的代码

        /*

         *由于getName()函数属于Thread类中的函数,而Thread类属于MyThread2的父类

         *所以直接通过getName()函数就可以获得线程的名字,哪个线程调用run函数,就会

         *获得哪个线程的名字

         */

        for (int i = 0; i <10; i++) {

            System.out.println(super.getName()+"..."+i);

        }

    }

}

publicclass ThreadDemo2 {

 

    publicstatic void main(String[]args) {

        // C:创建线程类的对象

        MyThread2 mt = new MyThread2();

        // D:启动线程,执行任务

        mt.start();

        // 再创建一个线程对象

        MyThread2 mt2 =new MyThread2();

        // 再一次开辟一个新的线程

        mt2.start();

        for (int i = 0; i <10; i++) {

            System.out.println(i);

        }

    }

}




方式二:实现Runnable接口

代码如下所示:

/*

 * 演示实现多线程方式2:实现Runnable接口

 * A:定义一个类来实现Runnable接口,这个类是任务类

 * B:实现Runnable接口中的run函数

 * C:创建任务类对象

 * D:创建线程类Thread的对象,并把任务类对象作为参数传递

 * E:启动线程

 */

//A:定义一个类来实现Runnable接口,这个类是任务类

class MyTask implements Runnable

{

    //B:实现Runnable接口中的run函数

    public void run() {

        for (int i = 0; i <10; i++) {

            /*

             * 由于这个类是任务类,不是线程类了,方式2与方式1不同之处就是

             * 将线程类和任务类分离了,所以要想获得调用run函数的线程类名字不能直接调用getName()函数了

             * 要想调用线程类Thread中的getName(),那么必须获得线程类的对象

             * 获得当前正在执行线程的对象可以通过currentThread()函数

             */

            System.out.println(Thread.currentThread().getName()+"==="+i);

        }

    }

}

public class ThreadDemo3 {

    public static void main(String[]args) {

        //C:创建任务类对象

        MyTask task = new MyTask();

//      D:创建线程类Thread的对象,并把任务类对象作为参数传递

        Thread t1 = new Thread(task,"锁哥");

        Thread t2 = new Thread(task,"助教");

        //E:启动线程

        t1.start();

        t2.start();

        for (int i = 0; i <10; i++) {

            System.out.println(Thread.currentThread().getName()+"==="+i);

        }

    }

}

1.2、多线程安全问题解决

以下三种方法:


1、同步代码块

加同步格式:

    synchronized(需要一个任意的对象(锁))

    {

代码块中放操作共享数据的代码。

}


/*

 * 售票,多个售票窗口卖100张票

 */

//定义线程任务类

class SellTicketTaskimplements Runnable

{

    //定义100张票

    private int tickets=100;

    //定义一个对象充当同步代码块上的锁

    private Objectobj = new Object();

    //实现run函数

    public void run()

    {

        //使用循环模拟一直卖票

        while(true)//t1 t2

        {

            //为了解决多线程的安全问题,给操作的共享资源代码加同步

            synchronized(obj)//t1进来 关上门 上锁,此时t2进不来

            {

                //判断是否还有余票

                if(tickets>0)

                {

                    //休眠1毫秒,模拟延迟

                    try {Thread.sleep(1);}catch (InterruptedException e) {e.printStackTrace();}

                    //有余票 使用打印语句来模拟卖票

                    System.out.println(Thread.currentThread().getName()+"出票:"+tickets);

                    //票数量-1

                    tickets--;

                }

            }

            //t1出来,释放锁,打开门 其他线程可以进入

        }

    }

}

public class SellTicketDemo {

    public static void main(String[]args) {

        //创建线程任务类对象

        SellTicketTask stt = new SellTicketTask();

        //创建线程对象,四个线程模拟四个窗口

        Thread t1 = new Thread(stt,"窗口1");

        Thread t2 = new Thread(stt,"窗口2");

        Thread t3 = new Thread(stt,"窗口3");

        Thread t4 = new Thread(stt,"窗口4");

        //启动线程

        t1.start();

        t2.start();

        t3.start();

        t4.start();

    }

}

2、同步方法

 

如上图所示,如果一个方法进来后,直接就是同步,也就是说,这个方法的所有代码都需要被同步。此时我们可以考虑把同步直接加到方法上: 

以上被synchronized关键字修饰的方法称为同步方法。

但是对于同步方法来说,并没有唯一的锁,这样会引发多线程安全问题。

上述代码封装到同步方法中后,会出现如下问题:


那么同步方法上的锁到底是什么呢?

2)同步方法上的锁

         同步方法的锁,不需要我们指定。证明,同步方法有自己默认的锁。

         这个锁肯定是一个对象。而这个对象是同步方法能拿到的。上述method方法是非静态方法,而任何非静态方法,都有一个隐含的对象,就是:this。

         所以,我们推测:同步方法的锁就是this

 

注意:

         A:如果一个方法内部,所有代码都需要被同步,那么就用同步方法;

         B:非静态同步方法的锁是this

3、静态同步方法

(1)既然有非静态同步方法,那么肯定也会有静态同步方法。

将上述非静态同步方法改为静态同步方法,代码如下所示:


问题:非静态同步方法有隐式变量this作为锁,那么静态方法中没有this,那么静态同步方法中的锁又是什么呢?

 

2)静态同步方法上的锁

         静态同步方法,没有this,所以不能用this来做锁。

 

         静态方法,类加载的时候,就已经被加载了。这个时候,锁已经确定了。

 

         静态同步方法的锁,肯定也是一个对象,而且这个对象应该是在类加载时就已经存在。

        

         静态同步方法的锁是:当前类的字节码文件对象(Class对象)

 

其实可以这么理解:什么是字节码文件对象呢?其实就是class文件,类名.class。比如这里,就是  SellTicketTask.class

 

         获取Class对象的方式:类名.class

说明:

类名.class整体表示一个对象,class是作为类名的一个静态属性存在。这个class属性是所有的类型都具备的,API不会去专门描述他,他本身就在所有的类型中存在。

代码如下所示:

总结:

         同步代码块:锁是任意对象,但是必须唯一;

         非静态同步方法:锁是this

         静态同步方法:锁是当前类的字节码文件对象;类名.class


1.3、同步的好处、弊端

线程安全问题出现的原因:

         A:多个线程

         B:多个线程操作共享资源

         C:操作资源的代码有多行

 

同步的弊端和好处:

同步的弊端:使用同步之后,会影响程序的执行效率。

                                      每次CPU运行到同步的地方,都需要去判断有没有线程在同步代码块中。

         同步的好处:加了同步之后,可以保证多个线程操作共享数据的安全。


1.4死锁问题

死锁:是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象。

注意:在开发中一旦发生了死锁现象,不能通过程序自身解决。必须修改程序的源代码。

      在开发中,死锁现象可以避免,但不能直接解决。当程序中有多个线程时,并且多个线程需要通过嵌套对象锁(在一个同步代码块中包含另一个同步代码块)的方式才可以操作代码,此时就容易出现死锁现象。

      可以使用一个同步代码块解决的问题,不要使用嵌套的同步代码块,如果要使用嵌套的同步代码块,就要保证同步代码块的上的对象锁使用同一个对象锁(唯一的对象锁)

原创粉丝点击