Java学习笔记之多线程 1

时间:2023-02-26 12:59:10

多线程学习笔记1

一、前言

随着CPU频率的增长出现停滞,CPU转向多核方向发展。多线程作为实现软件并发执行的一个重要方法,也就具有越来越重要的地位。

Java在多线程处理方面性能超群、功能强大。而且Java语言进行多线程处理很简单,所以程序员利用多线程技术能编写出非常有效率的程序来充分利用CPU,使CPU的空闲时间能够保持在最低限度。

进程是程序的一次动态执行过程,它经历了从代码加载、执行到执行完毕的一个完整过程,这个过程也是进程本身从产生、发展到最终消亡的过程。多任务操作系统能同时运行多个进程(程序),由于CPU具备分时机制,所以每个进程都能循环获得自己的CPU时间片。由于CPU执行速度非常快,使得所有程序好象是在“同时”运行一样。

线程也称为轻量级进程,是程序执行的最小单元。一个标准的线程由线程ID,当前指令指针PC,寄存器集合和堆栈组成。一般一个进程由一个到多个线程组成,各线程之间共享程序的内存空间(包括代码段、数据段、堆等)及一些进程资源(如打开文件和信号)。一个经典的线程与进程的关系如图1所示。

Java学习笔记之多线程 1

图1

图片来源:操作系统概念第四章第1节

    线程的并发执行:当线程数量小于处理器数量时(并且操作系统支持多处理器),线程的并发是正真的并发执行,但当线程的数量大于处理器的数量时,线程的并发会受到一些阻碍,因为此时至少有一个CPU会运行多个线程。

    线程调度:在单处理器对应多线程的情况下,并发是一种模拟出来的状态。操作系统会让这些多线程程序轮流执行,每次仅执行一小段时间(通常是几十到几百毫秒),这样每个线程看起来在同时执行。这种不断在处理器上切换不同的线程的行为称之为线程调度

    原子操作:当两个独立的进程对同一文件进行追加写操作时容易出现数据覆盖等一些逻辑问题,为了避免这种情况引入一种原子操作方法,把一条不会被打断的单指令操作称为原子操作。他们仅适用于比较简单的特定的场合。

    同步:一个线程在访问数据未结束时,其他线程不得对同一数据进行访问。

参考:或程序员的自我修养第一章第1.6节

    一个最简单的java程序在执行时,JVM启动至少启动了垃圾回收线程和主线程。

二、java多线程实现方式1   ,继承Thread类

   2.1简介

Thread类是在java.lang包中定义的,从JDK1.0开始。一个类只要继承了Thread类同时覆写了本类中的run()方法就可以实现多线程操作了,但是一个类只能继承一个父类,这是此方法的局限。

    2.2字段介绍

        public static final int MAX_PRIORITY //线程可以具有的最高优先级

public static final int MIN_PRIORITY//线程可以具有的最低优先级

public static final int NORM_PRIORITY//分配给线程的默认优先级

    2.3构造方法

       Thread()//分配新的Thread对象

       Thread(Runnabletarget)//分配新的 Thread 对象。

       public Thread(Stringname)/分配新的 Thread 对象,name:新线程的名称

    2.4常用方法

public final String getName()//返回该线程的名称

public final ThreadGroupgetThreadGroup()//返回该线程所属的线程组。 如果该线程已经终止(停止运行),该方法则返回null.

public final void setName(String name)//其实通过构造方法也可以给线程起名字

public static Thread currentThread()//获取任意方法所在的线程名称

public final void join()//等待该线程终止

public void run()//如果该线程是使用独立的Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回.Thread的子类应该重写该方法

public void start()//使该线程开始执行;Java 虚拟机调用该线程的run 方法。注意:多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。

public static void sleep(long millis)// 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)

public static void yield()//暂停当前正在执行的线程对象,并执行其他线程

创建线程方式1的步骤:

1)定义一个类继承Thread类

2)重写Thread类的run方法

3)创建线程对象

4)调用start方法,启动线程,同时告诉JVM去调用run方法。

注意:Thread对象不可以重复使用,一旦线程的run方法执行结束,该线程就不能再重新启动。Thread对象可能还在堆空间存在,如同活着的对象一般还能接受某些方法的调用,但已经永久的失去了线程的执行性,只剩下对象本身。

    2.5测试案例1:获取线程对象,获取线程名称

    //1、自定义类继承thread类

class Demoextends Thread {

    private String name;

    Demo(String name) {

       this.name = name;

    }

    //2、重写run方法

    public void run() {

       for(int x=1; x<=20; x++) {//注意 x是局部变量,为每个线程独享

            System.out.println("name="+name+"..."+Thread.currentThread().getName()+"..."+x);//获取当前线程名

       }

    }

}

//测试类

class  ThreadDemo {

    public static void main(String[] args) {

       //3、创建了两个线程对象。

       Demo d1 = new Demo("猫");

       Demo d2 = new Demo("狗");

       d2.start();//4、将d2这个线程开启。

       d1.run();//由主线程负责。

        }

}

输出结果:

name=猫...main...1

name=狗...Thread-1...1

name=猫...main...2

name=狗...Thread-1...2

name=猫...main...3

name=狗...Thread-1...3

name=猫...main...4

name=狗...Thread-1...4

name=猫...main...5

……

由输出结果可知主线程名称是:main,自定义线程名称是:Thread-1。线程多个时,数字顺延:Thread-2...

主线程和自定义线程在内存中的分配图,如图2所示。

Java学习笔记之多线程 1

图2

假设主线程先结束,那么其他线程会怎样?

由图2可知:一个进程中有多个线程“同时”执行时,每一个执行线程都有一片自己所属的栈内存空间,进行方法的压栈和出栈。当主线程结束了,主线程所在的栈内存自动释放。但是其他未执行完的线程任然在栈空间中继续执行,整个程序并未结束,因为线程之间是并列关系。只有当所有的线程都结束了,进程(程序)才结束。

    测试案例2:线程中的异常

    //自定义类继承Thread类

class Demo extends Thread {

    privateString name;

    Demo(Stringname) {

       this.name= name;

    }

       //重写run方法

    public voidrun() {

       //int[]arr = new int[3];

       //System.out.println(arr[4]);

       //自定义线程异常

       for(intx=1; x<=20; x++) {

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

       }

    }

}

//测试类

class  ThreadDemo{

    publicstatic void main(String[] args) {

       //创建了两个线程对象。

       Demo d1= new Demo("猫");

       Demo d2= new Demo("狗");

       d2.start();//将d2这个线程开启。

       d1.run();//由主线程负责。

       //int[]arr = null;

       //System.out.println(arr.length);

       //主线程异常

    }

}

注意:一旦线程中出现异常,且没有捕获异常时,会导致该异常所在的线程结束,与其他线程无关。

2.6常见问题

1、为什么不直接创建Thread类的对象,而是通过子类来创建线程?如下所示

Threadt = new Thread();

t.start();

通过查阅javaAPI可知,Thread类的run()方法是一个空方法,该方法并没有定义我们让程序执行的线程体,所以需要子类重写run方法。

         2、创建线程的目的是什么?

          是为了建立单独的执行体,让多部分代码实现“同时”执行。

         3、线程对象调用 run方法和调用start方法区别?

调用run方法不开启线程。仅是对象调用方法。

       调用start方法是开启线程,并让jvm调用run方法在开启的线程中执行

2.7线程生命周期

    如图3所示。

Java学习笔记之多线程 1

图3

三、java多线程实现方式2,实现Runnable接口

    简介:Runnable 接口由那些通过某一线程执行其实例的类来实现。类必须实现Runnable接口中 run 方法。从JDK1.0开始。

    构造方法:Runnable是一个接口,无构造方法

    常用方法

voidrun();//使用实现接口Runnable 的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的run 方法。

创建线程方式2的步骤:

1)定义一个类实现Runnable接口,避免了继承Thread类的单继承局限性

2)覆盖接口中的run方法,将要执行的线程体代码定义到run方法中。

3)创建Runnable子类对象,

4)创建Thread类的对象,将Runnable接口的子类对象作为参数传递给Thread类的构造函数。

5) 调用Thread类的start方法开启线程。

推荐使用方式2创建线程。

常见问题:创建线程的两种方式以及区别?

方式1继承Thread类创建线程时有单继承的局限性。

方式2实现Runnable接口避免了单继承的局限性,所以较为常用。

方式2实现Runnable接口,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。

方式1继承Thread类:线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。

    测试案例1:卖票小程序,创建线程的第二种方法。

    //1、自定义类实现runnable接口

class Ticket implements Runnable {

    //描述票的数量。

    private inttickets = 100;//注意tickets是共享数据,多个线程共同使用同一个tickets。

    //2、重写接口中的run方法,实现线程体:售票的动作。

    public voidrun() {

       //线程任务中通常都有循环结构

       while(true){//注意:由于是死循环,程序运行完毕后,需要手动关闭

           if(tickets>0) {//如果票数大于0就开始卖票

              System.out.println(Thread.currentThread().getName()+"....."+tickets--);//打印线程名称。

           }

       }

    }

}

//测试类

class ThreadDemo3 {

    public staticvoid main(String[] args) {

       //3、创建Runnable接口的子类对象。

       Ticket t= new Ticket();

       //4、创建四个线程对象。并将Runnable接口的子类对象作为参数传递给Thread的构造函数。

       Threadt1 = new Thread(t);

       Threadt2 = new Thread(t);

       Threadt3 = new Thread(t);

       Threadt4 = new Thread(t);

       //5、开启四个线程。

       t1.start();

       t2.start();

       t3.start();

       t4.start();

       //注意同一个线程不能连续启动多次。好比刘翔在做跨栏训练,教练打了一次令枪,刘翔开始跨栏,此时教练又打了3枪,刘翔就疯了,不知道教练到底要干嘛。

//例如:t1.start();t1.start();t1.start();t1.start();是错误的。

    }

}

输出结果

...

Thread-1.....5

Thread-2.....10

Thread-1.....4

Thread-1.....2

Thread-3.....6

Thread-0.....7

Thread-1.....1

Thread-2.....3

注意:多线程时,输出结果是随机的,无序的。

四、多线程安全问题

多线程安全问题产生的原因:当多个线程操作同一个共享数据时,一个线程只执行了一部分代码,还没有执行完,另一个线程参与进来执行,把共享数据的值改变了,导致输出结果错误。

测试案例2:多线程安全问题的发生  

和测试案例1代码类似,只需在自定义类的run方法中加入sleep()延时即可模拟问题的发生。

//1、自定义类

class Ticket implements Runnable {

//注意tickets是共享数据,多个线程共同使用同一个tickets,容易发生错误现象。

    private inttickets = 100;

    //2、重写run()方法

public void run()    {

       while(true){

//让线程一直跑着,即使线程内的循环条件结束,该线程也不会退出,所以在输出控制台需要手动的关闭程序。

           if(tickets>0) {

           //要让线程在这里睡10ms,模拟问题的发生。输出时会看到:0、-1、-2 错误的数据,这就是多线程安全问题。

              try{

Thread.sleep(10);//run()方法不解释抛出异常,所以只能用try...catch处理sleep可能出现的异常。

              }catch(InterruptedExceptione){

                  //暂不处理异常

              }     

System.out.println(Thread.currentThread().getName()+"....."+tickets--);//打印线程名称。

           }

       }

    }

}

//测试类

class ThreadDemo3 {

    publicstatic void main(String[] args) {

       //3、创建Runnable接口的子类对象。

       Ticket t= new Ticket();

       //4、创建四个线程对象。将Runnable接口的子类对象作为参数传递给Thread的构造函数。匿名对象方式

       newThread(t).start();

        new Thread(t).start();

       newThread(t).start();

       newThread(t).start();

    }

}

输出结果:

...

Thread-1.....1

Thread-0.....0

Thread-2.....-1

Thread-3.....-2

由输出结果可知,在线程体内加入sleep()函数后,程序卖出了第0、-1、-2,3张错误的票。原因分析:假设tickets=1,Thread-0开始执行线程体,当执行到sleep()函数时,被中断进入阻塞状态,执行权被Thread-1抢去。Thread-1执行到sleep()函数时,也进入阻塞状态,执行权被Thread-2抢去。当Thread-2执行到线程体的sleep()函数时,也被进入阻塞状态,执行权被Thread-3抢去。当Thread-3执行到线程体的sleep()函数时,也被进入阻塞状态,假设此时Thread-0醒来,并抢到CPU执行权,Thread-0执行完线程体后打印出:Thread-0.....1(tickets=0),假设此时Thread-1醒来,并抢到CPU执行权,Thread-1执行完线程体后打印出:Thread-1.....0(tickets=-1),以此类推,Thread-2打印出:Thread-2.....-1(tickets=--2),Thread-3打印出:Thread-3.....-2(tickets=--3),当线程Thread-3执行完毕后,4个线程全部都执行完线程体,此时4个线程开始重新抢夺CPU的执行权,但是无论哪个线程抢到CPU的执行权都无法进入线程体中,因为tickets= -3,不满足if()语句的条件。线程执行过程如图4所示。

Java学习笔记之多线程 1

图4

    测试案例3:多线程安全问题的解决。

    测试案例2的输出结果出现了负数票,如何避免这种现象?只要让一个线程在操作共享数据时,不让其他线程参与运算即可。

方式一:使用同步代码块synchronized来实现。

    格式:

synchronized(对象) {

    //需要被同步的代码。

}

测试代码

classTicket implements Runnable {

    private int tickets = 1000;//共享数据

    private Object obj = new Object();//接受任意类型的对象

    public void run()    {

       while(true) {

           //同步代码块,解决多个线程操作共享数据的安全问题。

           synchronized(obj)   {

           //此处相当于有一把“对象锁”(默认为开),当某线程进入同步代码块后,就把该“对象锁”关闭,其他线程就不能进入了。当该线程执行结束后,又把“对象锁”打开。

              if(tickets>0) {

                  //注意:run方法内的代码不能抛出异常,只能用try...catch

                  try{

Thread.sleep(10);//让线程睡10ms,模拟问题的产生

}catch(InterruptedExceptione){

//不处理sleep()函数的异常

}

                  System.out.println(Thread.currentThread().getName()+"....."+tickets--);//打印线程名称。

              }

           }

       }

    }

}

//测试类和测试案例2中的测试类一致,此处略

方式二:使用同步函数实现,把synchronized关键字放到需要同步的方法上。

格式:

public synchronized void xxx(){

    //需要同步的代码

}

测试代码

class Ticket implements Runnable{

    private  int tick = 1000;

    // synchronized不能直接放在run()方法上

    public  void run()   {

       while(true){

           show();//间接调用同步代码函数

       }

    }

    public synchronized void show() {

       if(tick>0)   {

           try{

Thread.sleep(10);//让进来的线程睡10ms,模拟问题的产生

}catch(Exception e){

    //不处理异常

}

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

       }

    }

}

//测试类和测试案例2中的测试类一致,此处略.

输出结果:略

方式三:JDK1.5出现的新特新 lock接口。

简介Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的Condition 对象。常用子类ReentrantLock:重入锁。

常用方法:

publicvoid lock()//获取锁

public  void unlock()//释放锁

格式:

     Lock l = ...;

     l.lock();

     try {

         // access the resource protected bythis lock

     } finally {

         l.unlock();

     }

测试代码:卖票小程序

//1、自定义类实现runnable接口

public class Ticket implements Runnable {

    //定义票

    int ticket = 100;

    //定义锁对象:父类引用指向子类对象

    Lock ck = new ReentrantLock();

    //2、重写run方法

    public void run() {

       while (true) {

           ck.lock();//上锁

              if (ticket > 0 ) {

                     try {

                         Thread.sleep(10);

                      } catch(InterruptedExceptione) {

                         // TODO Auto-generated catch block

                         e.printStackTrace();

                  }

                  System.out.println(Thread.currentThread().getName() +"正在卖票 :" +ticket--);

              }

           ck.unlock();//解锁

       }

    }

}

//测试类

public class TicketThread {

    public static void main(String[] args) {

       //3、创建自定义类对象

       Ticket ticket = new Ticket();

       //4、创建线程对象

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

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

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

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

       //5、启动线程

       t1.start();

       t2.start();

       t3.start();

       t4.start();

    }

}

由上述3个同步方法比较可知,虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,而JDK5以后提供了一个新的锁对象Lock,则可以清晰的表达如何加锁和释放锁。

测试案例3中的同步锁就类似于火车上的卫生间,当有人进入卫生间后把门锁上时(对象锁关闭),其他人只能在门口等待,当这个人上完厕所后,把门打开出去(对象锁打开),其他人才可以进去...

同步的好处:解决多线程操作共享数据时的安全问题。

同步弊端:每个线程都要判断锁,降低了程序的执行速度。

同步前提:必须要有两个或者两个以上的线程;必须是多个线程使用同一个锁对象。

测试案例4:同步代码块和同步函数的锁必须一致

//1、自定义类实现runnable接口

classTicket implements Runnable {

    private int tick = 100;

    Object obj = new Object();

    boolean flag = true;//根据标志位,进入同步代码块或同步函数

    //2、重写run()方法

    public void run() {

       //同步代码块

       if(flag) {

           while(true) {

              //synchronized(obj) {

//如果多线程时,如果同步代码块的锁和同步函数的锁不一致,在操作共享还是会出现错误的现象。

              synchronized(this) {

//同步代码块和同步函数的锁是同一把锁,才能避免操作共享数据出错的情况。

                  if(tick>0){

                     try{

                         Thread.sleep(10);

                     }catch(Exception e){

                         //不处理异常

                     }

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

                  }

              }

           }

       }

       //同步函数

       else{

           while(true){

              show();

           }

        }

    }

    //同步函数锁是this

    public synchronized void show() {

       if(tick>0) {

           try{

              Thread.sleep(10);

           }catch(Exception e){

              //不处理

           }

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

       }

    }

}

//测试类

class  ThisLockDemo {

    public static void main(String[] args) {

       //3、创建自定义对象

       Ticket t = new Ticket();

       //4、创建线程对象

       Thread t1 = new Thread(t);

       Thread t2 = new Thread(t);

       //5、启动线程

       t1.start();//线程t1启动后,

       try{

           Thread.sleep(10);//让主线程睡10ms,让t1先运行

       }catch(Exception e){

           //不处理异常

       }

       t.flag = false;//线程t1在执行的过程中,主线程醒来,把标志位置为假。

       //启动线程

       t2.start();

    }

}

注意:同步函数的锁是this,在同一个run方法中同步代码块和同步函数的锁必须一致,才能避免出现操作共享数据出错的情况。

测试案例5:静态同步函数的锁是什么

classTicket implements Runnable {

    private static  int tick = 100;

    boolean flag = true;

    public void run()   {

       if(flag) {

           while(true) {

              //静态同步代码块的锁必须和静态同步函数的锁一致,所以静态代

              码块的锁也是:类名.class

              synchronized(Ticket.class){

                  if(tick>0) {

                     try{Thread.sleep(10);}catch(Exceptione){}

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

                  }

              }

           }

       }

       else{        

           while(true)

              show();

       }

    }

    //静态同步函数锁是类名.class

    publicstatic synchronized void show(){

       if(tick>0) {

           try{Thread.sleep(10);}catch(Exceptione){}

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

       }

    }

}

//测试类和测试案例4的测试类一致,此处略。

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

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

实际工程中一般不建议使用静态同步函数,因为他的生命周期太长,一直占用内存空间(静态在内存的数据区分配),直到整个程序执行结束,静态函数才销毁。

五、死锁问题

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

同步代码块的嵌套就是经典的死锁现象。

    测试代码:写一个死锁的代码

//1、自定义类

class Test implements Runnable {

    privateboolean flag;//标志位

    Test(booleanflag){//构造方法

       this.flag= flag;

    }

    //2、重写run方法

    public voidrun() {

       if(flag){//根据标志位进入不同的同步代码块中

           while(true){

              //同步代码块嵌套同步代码块

              //同步代码块1,锁a

              synchronized(MyLock.locka){

                  System.out.println(Thread.currentThread().getName()+"...if locka");//同步代码块1的语句

                  //同步代码块2,锁b

                  synchronized(MyLock.lockb){

                     System.out.println(Thread.currentThread().getName()+"..iflockb");//同步代码块2的语句

                  }

              }

           }

       }

       else {//根据标志位进入不同的同步代码块中

           while(true){

              //同步代码块2,锁b

              synchronized(MyLock.lockb){

                  System.out.println(Thread.currentThread().getName()+"..elselockb");//同步代码块2的语句

                  //同步代码块1,锁a

                  synchronized(MyLock.locka){

                     System.out.println(Thread.currentThread().getName()+".....elselocka");//同步代码块1的语句

                  }

              }

           }

       }

    }

}

//锁对象

class MyLock {

    staticObject locka = new Object();//锁对象,加static方便调用

    staticObject lockb = new Object();

}

//测试类

class DeadLockTest {

    publicstatic void main(String[] args) {

       //3、匿名方式创建线程对象

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

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

       //4、开启线程

       t1.start();

       t2.start();

    }

}

输出结果:

Thread-0...if locka

Thread-1..else lockb

由输出结果可知,程序处于假死,等待状态,这是因为线程1持有自己的锁,想进入线程2中,而线程2只有自己的锁,想进入线程1中,这两个线程互相要对方的锁,但又不释放自己的锁,所以他们一直僵持着...看上去就像程序“卡”住了一样。

    死锁出现的原因:同步代码块中嵌套同步,但这两个同步的锁不一样。实际开发中尽量避免出现死锁。