Java笔记3 多线程<1>线程概述、多线程的创建、多线程的安全问题、静态同步函数的锁、死锁

时间:2021-10-14 19:36:12


11-01-多线程概述

l  进程定义:进程是一个正在执行中的程序。每个进程都有一个执行顺序,该执行顺序做

控制单元。

l  线程定义:线程是进程中独立的一个控制单元。线程在控制着进程的执行。

一个进程中至少有一个线程。

²  多线程存在的意义:多线程的意义在于一个应用程序的多个逻辑单元可以并发地执行。

但是多线程并不意味着多个用户进程在执行,操作系统也不把每个线程作为独立的进程来分配独立的系统资源。进程可以创建其子进程,子进程与父进程拥有不同的可执行代码和数据内存空间。而在用于代表应用程序的进程中多个线程共享数据内存空间,但保持每个线程拥有独立的执行堆栈和程序执行上下文

 

 

11-02-创建多线程

l  创建线程有两种方式:继承Thread类和实现Runnable接口。

    第一种方式:继承Thread类。

步骤:

1.定义继承 Thread类。

2.复写Thread类中的run方法。

3.调用线程的start方法。

 

代码示例:

class Demo extends Thread//继承1.Thread类

{

public voidrun()//2.复写run方法

{

for(int i=0;i<60;i++)

{

System.out.println(“Demo run--”+i);

}

}

}

 

public class ThreadDemo

{

public staticvoid main(String[] args)

{

Demo d = new Demo();

d.start();//3.开启线程并执行run方法

//d.run();//仅仅是对象调用方法,而线程创建了并没有执行。

}

 

for(inti=0;i<60;i++)

{

System.out.println(“main--”+i);

}

}

 

多次运行后每一次运行结果都不一样。

原因:多个线程都获取到cpu的执行权,cpu执行到谁,谁就运行。在某一时刻,只有一个程序在运行(多核除外),cpu做着快速的切换,已达到看上去是同时运行的效果,我们可以形象地把多线程的运行行为看成是在互相抢夺cpu的执行权。

²  结论:多线程的一个特点:随机性。谁抢到谁就执行,至于执行多长时间,取决于cpu。

 

 

11-03-线程run和start特点

l  线程run和start的特点

run方法的特点:用于储存线程要运行的代码。

start方法的特点:启动线程并执行该线程的run方法(在启动线程后不一定竞争到cpu的执行权)。

 

 

11-08-创建线程-实现Runnable接口

l  创建线程的第二种方法:实现Runnable接口

步骤:

1.定义类实现Runnable接口。

2.覆盖Runnable接口中的run方法(将线程要运行的代码存放在run方法中)。

3.建立Runnable接口的子类对象。

4.将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。

5.调用Thread类的start方法开启并调用Runnable接口。

 

代码示例:

class Ticket implements Runnable//1.实现Runnable接口

{

private int tick= 100;

public voidrun()//2.重写run方法

{

while(true)

{

if(tick>0)

{

System.out.println(Thread.currentThread().getName()+”...sale”+tick--);

}

}

}

}

 

class TicketDemo

{

public staticvoid main(String[] args)

{

Ticket t = new Ticket();//3.创建Runnable接口的子类对象

 

//4.将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。

thread t1 = new Thread(t);

thread t2 = new Thread(t);

thread t3 = new Thread(t);

thread t4 = new Thread(t);

 

//5.调用Thread类的start方法开启线程并调用Runnable接口子类中的run方法

t1.start();

t2.start();

t3start();

t4.start();

}

}

 

l  实现Runnable接口的好处:避免了单继承的局限性,适合于资源共享。

 

 

11-09-多线程的安全问题

通过对11-08代码示例运行结果的分析,出现了打印出0,-1,-2等错票。

11-08示例代码运行结果分析:

(1)运行run方法有四个线程t1、t2、t3、t4。首先t1获取cpu执行权。

(2)当tick = 1时,刚判断完,t1卧倒(t1具备执行资格,但是此刻cpu执行权被其他线程抢走了或是切换到其他程序去了),t1并没有执行if语句后面代码块中的语句。接着t2获得了cpu执行权,这时的tick = 1,刚判断完,t2也卧倒。t3、t4如同上述情况也处于卧倒状态。

(3)某一时刻,t1获取到了cpu执行权,执行if语句代码块,tick--,tick = 0;

(4)按理说tick = 0,if语句代码块就不在执行,但是t2、t3、t4已经过判断,有执行if语句代码块的资格。t1运行完毕,t2、t3、t4不在经过if语句判断,t2获取cpu执行权,执行if语句代码块,打印0号票,tick--;t2运行完毕,t3获取cpu执行权,打印-1号票,tick--;同理,t4运行打印-2号票。

 

l  问题的原因?

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

 

l  解决方法:

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

 

l  synchronized

同步的前提:

1.必须要有多个(两个或两个以上)线程。

2.必须是多个线程使用同一个锁。(注意:不同线程使用不同锁会出现线程安全问题)

3.必须保证同步中只有一个线程在运行。

 

synchronized的两种表现形式:

1.同步代码块。(对象锁的运用要准确,要不然会出现线程安全问题)

2.同步函数。(经过视频展示,同步函数要运用到位,要不然达不到多线程的目的,因为同步函数一次只能进入一个线程)

同步代码块示例:

synchronized(对象)

{

需要同步的代码块;

}

 

同步函数示例:

public synchronized int add(int a){

//需要同步的函数代码

}

 

²  synchronized的利与弊:

利:解决多线程的安全问题。

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

 

²  小收获:继承接口的run方法,调用sleep()等方法时只能try...catch,不能抛异常。因为接口中的run方法未抛异常。

 

 

11-12-多线程-同步函数的锁是this

l  同步函数用的是哪一个锁呢?

函数需要被对象调用,那么函数都有一个所属对象的引用,就是this。所以同步函数使用的锁是this。

如果一个类里面中既有同步函数又有同步代码块,示例如下:

class Ticket implements Runnable

{

private int tick= 100;

object obj = newObject();

boolean flag =true;

 

public voidrun()

{

if(flag)

{

while(true)

{

//如果synchronized的参数是obj而不是this,在两个或两个以上的线程执行过

//程中就会出现线程安全问题:打印0号票。

synchronized(obj)//<1>

{

if(tick>0)

{

System.out.println(Thread.currentThread().getName()+”...code”+tick--);

}

}

}

}

else

while(true)

{

show();//或是this.show()

}

}

 

publicsynchronized void show()//<2>

{

if(tick>0)

{

System.out.println(Thread.currentThread().getName()+”...show...”+tick--);

}

}

 

public staticvoid main(String[] args)

{

Ticket t = new Ticket();

 

Thread t1 = new Thread(t);

Thread t1 = new Thread(t);

t1.start();

try{Thread.sleep()10;}catch(Exception e){}

t2.start();

t.flag = false;

t2.start();

}

}

 

运行结果表明:在有非静态的同步函数和同步代码块的类中,只有同步代码块中的对象锁为this时,才不会打印出错票的情况。

 

 

11-13-多线程-静态同步函数的锁是class对象

l  如果同步函数被静态修饰符修饰后,使用的锁是什么的呢?

通过验证,发现不是this,因为静态方法中不可以定义this。

静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。就是

类名.class,该对象的类型是Class。

 

代码示例:

如果将11-12示例代码中的<2>处改为:public static synchronized voidshow(){...}

那么<1>处应该改为:synchronized(Ticket.class){...}

要不然对象锁不一致会出现线程安全问题:打印出0号票。

 

11-15-多线程-死锁

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。        导致死锁的根源在于不适当地运用“synchronized”关键字来管理线程对特定对象的访问,使同步中嵌套同步。

l  死锁代码举例:

class Test implements Runnable{

    private boolean flag;

    Test(boolean flag){

       this.flag = flag;

    }

 

    public void run(){

       if(flag){

           while(true){

              synchronized(MyLock.locka){

                  System.out.println(Thread.currentThread().getName()+"...if locka ");

                  synchronized(MyLock.lockb){

                     System.out.println(Thread.currentThread().getName()+"..if lockb");              

                  }

              }

           }

       }

        else{

           while(true){

              synchronized(MyLock.lockb){

                  System.out.println(Thread.currentThread().getName()+"..else lockb");

                  synchronized(MyLock.locka){

                     System.out.println(Thread.currentThread().getName()+".....else locka");

                  }

              }

           }

       }

    }

}

 

 

class MyLock{

    static Objectlocka =new Object();

    static Objectlockb =new Object();

}

 

class  DeadLockTest{

    public static void main(String[] args) {

       Thread t1 = new Thread(new Test(true));

       Thread t2 = new Thread(new Test(false));

       t1.start();

       t2.start();

    }

}

 

总结:采用多线程能充分利用CPU资源,提高代码的运行效率。但是在采用多线程的同时必须解决好线程的安全问题:代码同步,避免死锁的出现。