JAVA基础——多线程

Posted dfif

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA基础——多线程相关的知识,希望对你有一定的参考价值。

一、什么是线程

  • 进程:操作系统中每个独立执行的程序都可称为一个进程。
  • 线程:一个程序中能够同时运行的执行单元。

例:如果将QQ看作一个进程,则其中同时打开的聊天窗口则可看为一个个线程。

注:多线程程序运行时,每个线程之间是独立的,可以并发执行。但并不是同时执行,CPU同一时刻只能执行一个线程。

二、创建线程的两种方式

1、继承Thread类

Thread类位于java.lang包下,通过覆写Thread类的run()方法实现多线程。

示例:

 

public class Example_1 {
    public static void main(String[] args) {
        MyThread one = new MyThread();
        one.start();
        while(true){
            System.out.println("main()方法在运行!");
        }
    }
}

class MyThread extends Thread{
    //重写run()方法
    public void run(){
        while(true){
            System.out.println("MyThread类的run()方法在运行!");
        }
    }
}

 

注:有局限性,Java中一个类只能继承一个父类。

2、实现Runnable接口

其中只有一个run方法,通过构造函数Thread(Runnable target)创建线程对象。避免了局限性。

示例:

 

public class Example_2 {
    public static void main(String[] args) {
        MyThread_2 two = new MyThread_2();    //创建MyThread_2实例对象
        Thread thread = new Thread(two);      //创建线程对象
        thread.start();                       //开启线程
        while(true){
            System.out.println("main()方法在运行!");
        }
    }
}class MyThread_2 implements Runnable{
    public void run(){
        while(true){
            System.out.println("MyThread_2类的run()方法在运行");
        }
    }
}

 

3、两种方式的对

实现Runnable接口相对于继承Thread类来说有以下好处:

  ① 适合多个相同程序代码的线程取处理同一个资源的情况,把线程同程序代码,数据有效的分离,体现了面向对象的特点。
  ② 可以避免由于java的单继承带来的局限性。

示例:

public class Example_2 {
    public static void main(String[] args) {
        new TicketWindow().start();        //创建一个线程对象TicketWindow并开启
        new TicketWindow().start();        //创建一个线程对象TicketWindow并开启
        new TicketWindow().start();        //创建一个线程对象TicketWindow并开启
        new TicketWindow().start();        //创建一个线程对象TicketWindow并开启
        
        //TicketWindow_2 tw = new TicketWindow_2();    //创建TicketWindow_2实例对象
        //new Thread(tw,"窗口1").start();    //创建线程并命名,开启线程
        //new Thread(tw,"窗口2").start();    //创建线程并命名,开启线程
        //new Thread(tw,"窗口3").start();    //创建线程并命名,开启线程
        //new Thread(tw,"窗口4").start();    //创建线程并命名,开启线程
    }
}

class TicketWindow extends Thread{
    private int tickets = 100;
    public void run(){
        while(true){                    //通过死循环打印语句
            if(tickets > 0){
                Thread th = Thread.currentThread();        //获取当前线程
                String th_name = th.getName();             //获取当前线程的名字
                System.out.println(th_name + "正在发售第" + tickets-- + "张票");
            }
        }
        
    }
}

class TicketWindow_2 implements Runnable{
    private int tickets = 100;
    public void run(){
        while(true){                    //通过死循环打印语句
            if(tickets > 0){
                Thread th = Thread.currentThread();        //获取当前线程
                String th_name = th.getName();             //获取当前线程的名字
                System.out.println(th_name + "正在发售第" + tickets-- + "张票");
            }
        }
    }
}

 

4、后台线程

在java程序中,只要还有一个前台程序在运行,这个线程就不会结束,如果一个程序中只有后台线程运行,这个进程就会结束。

  ①新创建的线程默认都是前台线程
  ②如果某个线程对象在启动之前调用了setDaemon(true)语句,这个线程就变成一个后台线程。

 

注意:要将某个线程设置为后台线程,必须在该线程启动之前,也就是说setDaemon()方法必须在start()方法之前调用。否则会引发
IllegalThreadStateException异常。

代码:

class DamonThread implements Runnable{
    public void run(){
        while(true){
            System.out.println(Thread.currentThread().getName() + "---is running.");
        }
    }
}

public class Example_3 {
    public static void main(String[] args) {
        System.out.println("main线程是后台线程么?" + Thread.currentThread().isDaemon());
        DamonThread one = new DamonThread();               //创建一个DamonThread实例对象
        Thread thread = new Thread(one,"后台线程");         //创建线程thread共享one资源
        System.out.println("thread线程是后台线程么?" + Thread.currentThread().isDaemon());
        thread.setDaemon(true);                            //将线程thread设为后台线程
        thread.start();                                    //开启thread线程
        for(int i = 0; i < 10; i++){
            System.out.println(i);
        }
    }
}

 

三、线程的生命周期

从Thread对象创建完毕开始,当run()方法中代码正常执行完毕或者抛出一个未捕获的异常(Exception)或者错误(Error)时,线程的生命周期便会结束。

技术图片

 

 

 ①、新建状态(New)

创建一个线程后,该线程对象就处于就绪状态。仅仅由Java虚拟机为其分配了内存,不能运行。

 ②、就绪状态(Runnable)

当线程对象调用了start()方法后,该进程就进入就绪状态。处于就绪状态的线程位于可运行池中,此时它只是具备了运行的条件。

 ③、运行状态(Running)

如果处于就绪状态的线程获得了CPU的使用权,开始执行run()方法中的线程执行体,则该线程处于运行状态。

注:只有处于就绪状态的线程才可能转换到运行状态。

 ④、阻塞状态(Blocked)

一个正在执行的线程在某些特殊情况下,如执行耗时的输入/输出操作时,会放弃CPU使用权,进入阻塞状态。

注:线程进入阻塞状态后就不能进入排队队列。只有当引起阻塞原因被消除后,线程才可以转入就绪状态。

线程由运行状态转化成阻塞状态的原因,及如何从阻塞状态转换为就绪状态:

  • 当线程需要从其他线程获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程进入阻塞状态。如果想从阻塞状态进入就绪状态必须得获取到其他线程所持有的锁。
  • 当线程调用了一个阻塞式的IO方法时,该线程进入阻塞状态,如果想进入就绪状态就必须等到这个阻塞的IO方法返回。
  • 当线程调用了某个对象的wait()方法时,该线程进入阻塞状态,可使用notyfy()方法唤醒该线程使其进入就绪状态。
  • 当线程调用了Thread的sleep(long millis)方法时,线程进入阻塞状态,需等线程睡眠时间到了以后,线程就会自动进入就绪状态。
  • 当在一个线程中调用了另一个线程的join()方法时,会使当前线程进入阻塞状态,需要等到新加入的线程运行结束后才会结束阻塞状态进入就绪状态。

 ⑤、死亡状态(Terminated)

线程的run()方法正常执行完毕或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。

注:一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他其他状态。

 

四、线程的调度

即Java虚拟机按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制称为线程的调度。

1、线程的优先级

线程的优先级用1-10之间的整数来表示,数字越大优先级越高。可以用Thread类中提供的静态常量来表示线程的优先级。

Thread类的优先级常量:

  static int MAX_PRIORITY    表示线程的最高优先级,相当于值10

  static int MIN_PRIORITY    表示线程的最低优先级,相当于1

  static int NORM_PRIORITY     表示线程的普通优先级,相当于5  

注:main线程具有普通优先级。可以通过Thread类的setPriority(int newPriority)方法对其进行设置。

示例:

class MaxPriority implements Runnable{
    public void run(){
        for(int i=0; i<=10; i++){
            System.out.println(Thread.currentThread().getName() + "正在输出" + i);
        }
    }
}

class MinPriority implements Runnable{
    public void run(){
        for(int i=0; i<=10; i++){
            System.out.println(Thread.currentThread().getName() + "正在输出" + i);
        }
    }
}

public class Example_4 {
    public static void main(String[] args) {
        //创建两个线程
        Thread minPriority = new Thread(new MinPriority(),"优先级较低的线程");
        Thread maxPriority = new Thread(new MaxPriority(),"优先级较高的线程");
        minPriority.setPriority(Thread.MIN_PRIORITY);
        maxPriority.setPriority(10);
        //开启两个线程
        maxPriority.start();
        minPriority.start();
    }
}

2、线程休眠

可以通过静态方法sleep(long millis)将CPU让给别的线程,该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。

注:sleep(long millis)方法声明抛出InterruptedException异常,因此调用该方法时应该捕捉异常,或者声明抛出该异常。

示例:

class SleepThread implements Runnable{
    public void run(){
        for(int i=0; i<=10; i++){
            if(i == 3){
                try{
                    Thread.sleep(200);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }    
            System.out.println("线程一正在输出:" + i);
            try{
                Thread.sleep(500);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
public class Example_5 {
    public static void main(String[] args) throws InterruptedException {
        //创建一个线程
        new Thread(new SleepThread()).start();
        for(int i=1; i<=10; i++){
            if(i == 5){
                Thread.sleep(2000);
            }
            System.out.println("主线程正在输出" + i);
            Thread.sleep(500);
        }
    }
}

注:sleep()是静态方法,只能控制当前正在运行的线程休眠,而不能控制其他线程休眠。

3、线程让步

线程让步可以通过yield()方法来实现,该方法和sleep()方法有点相似,区别是yield()方法不会阻塞该线程,它只是将线程转换成就绪状态让系统重新调度一次。

注:当某个线程调用yield()方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。

示例:

class YieldThread extends Thread{
    public YieldThread(String name){
        super(name);                //调用父类构造方法
    }
    public void run(){
        for(int i=0; i<=500; i++){
            System.out.println(Thread.currentThread().getName() + "---" + i);
            if(i == 3){
                System.out.println("线程让步:");
                Thread.yield();        //线程运行到此,做出让步    
            }    
        }
    }
}

public class Example_6 {
    public static void main(String[] args) {
        //创建两个线程
        Thread one = new YieldThread("线程A");
        Thread two = new YieldThread("线程B");
        //开启线程
        one.start();
        two.start();
    }
}

4、线程插队

在Thread类中提供了一个join()方法来实现“插队”,即当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完毕后它才会继续运行。

示例:

class EmergencyThread implements Runnable{
    public void run(){
        for(int i=0; i<10; i++){
            System.out.println(Thread.currentThread().getName() + "输入:" + i);
            try{
                Thread.sleep(500);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
public class Example_7 {
    public static void main(String[] args) throws Exception {
        //创建线程
        Thread thread = new Thread(new EmergencyThread(),"线程一");
        thread.start();                    //开启线程
        for(int i=0; i<10; i++){
            System.out.println(Thread.currentThread().getName() + "输入:" + i);
            if(i == 2){
                thread.join();            //调用join()方法
            }
            Thread.sleep(500);
        }
    }
}

 

五、多线程同步

为了解决多个线程访问统一个资源时引发的一些安全问题。

解决方法:实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。

1、同步代码块

同步代码块:同步机制
 当多个线程使用一个共享资源时,可以将处理共享资源的代码放在一个代码块中,使用synchronized关键字来修饰,被称作同步代码块

语法格式:
 synchronized(lock){
     操作共享资源代码块
 }
 * #lock是一个锁对象是同步代码块的关键
 * #锁对象的类型可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的
 * #锁对象的创建代码不能放到run()方法中。

示例:

class SaleThread implements Runnable{
    private int tickets = 10;
    Object lock = new Object();                //定义任意一个对象,作为同步代码块的锁
    public void run(){
        while(true){
            synchronized(lock){                //定义同步代码块
                try{
                    Thread.sleep(10);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                if(tickets > 0){
                    System.out.println(Thread.currentThread().getName() + "---卖出的票" + tickets--);
                }else{
                    break;
                }
            }
        }
    }
}
public class Example_8 {
    public static void main(String[] args) {
        SaleThread one = new SaleThread();        //创建saleThread对象
        //创建并开启四个线程
        new Thread(one,"线程一").start();
        new Thread(one,"线程二").start();
        new Thread(one,"线程三").start();
        new Thread(one,"线程四").start();
    }
}

2、同步方法

方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,能实现和同步代码块相同的功能。

具体格式为:

synchronized 返回值类型 方法名([参数 1, ......]){}

被synchronized修饰的方法某一时刻值允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行方法。

示例:

class Ticket1 implements Runnable{
    private int tickets = 10;
    public void run(){
        while(true){
            saleTicket();
            if(tickets <= 0){
                break;
            }
        }
    }
    private synchronized void saleTicket(){
        if(tickets > 0){
            try{
                Thread.sleep(10);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "---卖出的票" + tickets--);
        }
    }
}
public class Example_9 {
    public static void main(String[] args) {
        Ticket1 ticket = new Ticket1();
        //创建并开启四个线程
        new Thread(ticket,"线程一").start();
        new Thread(ticket,"线程二").start();
        new Thread(ticket,"线程三").start();
        new Thread(ticket,"线程四").start();
    }
}

注:同步方法也有锁,它的锁就是当前调用该方法的对象,也就是this指向的对象。

优点:同步方法被所有线程所共享,方法所在的对象相对于所有线程来说是唯一的,从而保证了锁的唯一性。

3、死锁问题

死锁问题:两个线程运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为死锁。

示例:

public class Example_10 {
    public static void main(String[] args) {
        //创建两个DeadLockThread对象
        DeadLockThread one = new DeadLockThread(true);
        DeadLockThread two = new DeadLockThread(false);
        //创建并开启两个线程
        new Thread(one,"Chinese").start();
        new Thread(two,"American").start();
    }
}

class DeadLockThread implements Runnable{
    static Object chopsticks = new Object();        //定义Object类型的chopsticks对象
    static Object knifeAndFork = new Object();        //定义Object类型的knifeAndFork对象
    private boolean flag;                            //定义布尔类型变量flag
    
    DeadLockThread(Boolean flag){                
        this.flag = flag;
    }

    public void run() {
        if(flag){
            while(true){
                synchronized(chopsticks){            //chopsticks锁对象上的同步代码块
                    System.out.println(Thread.currentThread().getName()+"---if---chopsticks");
                    synchronized(knifeAndFork){
                        System.out.println(Thread.currentThread().getName()+"---if---knifeAndFork");
                    }
                }
            }
        }else{
            while(true){
                synchronized(knifeAndFork){            //knifeAndFork锁对象上的同步代码块
                    System.out.println(Thread.currentThread().getName()+"---else---knifeAndFork");
                    synchronized(chopsticks){
                        System.out.println(Thread.currentThread().getName()+"---else---chopsticks");
                    }
                }
            }
        }
    }
}

以上是关于JAVA基础——多线程的主要内容,如果未能解决你的问题,请参考以下文章

java基础入门-多线程同步浅析-以银行转账为样例

java多线程基础

Java基础之多线程

Java多线程基础

多线程编程学习一(Java多线程的基础)

Java 多线程基础多线程的实现方式