Java多线程基础

概述

 Java中的main(){}是Java程序的主线程,所有的分线程都在main(){}中开始,在main(){}中结束。
 有4种创建线程的方式:①继承Thread类;②实现Runnable接口;③实现Callable<>泛型接口;④线程池。

创建线程

继承Thread

MyThread.java

public class MyThread extends Thread {
    @Override
    public void run() {
        //some statement
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        MyThread thread_1 = new MyThread();
        MyThread thread_2 = new MyThread();
        
        thread_1.start();
        thread_2.start();
        
    }
}

注意:

①线程逻辑在run(){}中设计,因此Thread子类必须重写run(){}
②创建线程的流程为:创建Thread子类的实例⇨调用实例的start()
③运行当中的线程不能再次调用start(),否则start()会抛IllegalThreadStateException。例:

MyThread thread = new MyThread();
thread.start();
thread.start();  //throws IllegalThreadStateException

start()的作用:1.启动线程;2.调用run()

实现Runnable接口

Run.java

public class Run implements Runnable {
    @Override
    public void run() {
        for(int i=0;i<100;i++){
            if(i%2==0){
                System.out.println(i+" 是偶数");
            }
        }
    }
}

Main.java

public class Main {
    public static void main(Sting[] args){
        Thread thread = new Thread(new Run());
        thread.start();
    }
}

注意:

①此时调用的Thread类的构造器为:

public Thread(Runnable target) {...}

②线程逻辑在Runnable接口实现类的run(){}中设计。

③使用Thread对象的start()启动线程并调用run()

④继承Thread类与实现Runnable接口这两种方式的区别在于类与接口的区别。定义类时只能继承一个父类,而继承Thread类的方式中,Thread类会占据唯一父类;定义类时可以实现多个接口,因此实现Runnable接口比继承Thread类更好。

实现Callable<>泛型接口

代码示例

Call.java

public class Call implements Callable<String> {
    @Override
    public String call() throws Exception {
        for(int i=0;i<100;i++){
            if(i%2==0){
                System.out.println(i+" 是偶数");
            }
        }
        return "线程结束";
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        Call call = new Call();
        FutureTask<String> task = new FutureTask<>(call);
        Thread thread = new Thread(task);
        thread.start();
        try{
            String message = task.get();
            System.out.println(message);
        }
        catch(Exception e){
            System.out.println(e);
        }
    }  
}

Callable<>泛型接口

Callable<>接口可带1个泛型参数。Callable<>的定义为:

public interface Callable<V> {
    V call() throws Exception;
}

call()的返回类型即为Callable<>被实现时传入的泛型参数类型。

 使用Callable<>接口方式需要借助FutureTask<>泛型类。使用FutureTask<>对象的get()可获得call()的返回值,get()返回值类型与FutureTask<>传入的泛型参数类型一致。

Thread类中的常用方法

public static Thread currentThread()

1.作用:获取当前运行的线程。
2.返回:对应线程的Thread对象。
3.注意:currentThread()Thread类中的静态方法,必须在线程的逻辑执行体中调用才能获取对应的线程对象。对于主线程,Thread.currentThread()必须在main(){}中调用;对于继承Thread类和实现Runnable接口创建线程的方式,Thread.currentThread()必须在run(){}中调用;对于实现Callable<>泛型接口创建线程的方式,Thread.currentThread()必须在call(){}中调用。
4.示例:

public class Main {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
    }
}
public class Thr extends Thread {
    @Override
    public void run() {
        System.out.println(currentThread().getName());
    }
}
public class Run implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
public class Call implements Callable<String> {
    @Override
    public String call() {
        return Thread.currentThread().getName()
    }
}

public final String getName()

1.作用:获取线程名。
2.返回:String,线程名。
3.注意:若未设置线程名,系统默认按照线程的创建顺序分配线程名为:Thread-0,Thread-1,Thread-2…主线程的默认线程名为main

public final void setName(String name)

1.作用:设置线程名。
2.示例:

Call.java

public class Call implements Callable<String> {
    @Override
    public String call() {
        for(int i=0;i<100;i++) {
            System.out.println(Thread.currentThread().getName()+" output "+i);  
        }
        return null;
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new FutureTask<String>(new Call()));
        thread.setName("CallThread");
        thread.start();
    }
}

public static void yield()

1.作用:线程主动释放当前CPU执行权,下一时刻该线程可能重新获得CPU执行权。
2.注意:yield()Thread类中的静态方法,必须在线程的逻辑执行体中调用,例如main(){},run(){}call(){}
3.示例:

public class Call implements Callable<String> {
    @Override
    public void String call() {
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+" output "+i);
            if(i%20==0){
                Thread.yield();
            }
        }
    }
}

public final void join()

1.作用:调用线程等待目标线程死亡,之后继续执行。例如在线程a中调用b.join(),线程a将暂停等待,直到线程b死亡之后线程a才继续执行。
2.注意:join()会抛InterruptedExceptionjoin()不会使线程得到同步监视器或失去同步监视器,不会使线程阻塞,而是使线程处于等待状态(WAITING/TIMED_WAITING)。
3.扩展:join()的带参版本1:public final void join(long millis)。调用线程等待目标线程死亡的最长时间为millis毫秒。直到到达最长等待时间或者目标线程提前死亡之后,调用线程才继续执行。若millis=0,作用与join()相同。join(long millis)会抛InterruptedException
join()的带参版本2:public final void join(long millis,int nanos)。在join(long millis)的基础上增加毫秒和纳秒精度,nanos的范围为0-999999。若millisnanos均为0,作用与join()相同。join(long millis,int nanos)会抛InterruptedException
4.示例:

Call.java

public class Call implements Callable<String> {
    @Override
    public String call() {
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+" output "+i);
        }
        return "this is a test";
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new FutureTask<String>(new Call()));
        thread.setName("CallThread");
        Thread.currentThread().setName("MainThread");
        thread.start();
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+" output "+i);
            if(i==10){
                try{
                    thread.join(0,100);
                } catch(Exception e) {
                    System.out.println("some error occur");
                }
            }
        }
    }
}

stop()

 强制结束一个线程。此方法已过时,不推荐使用。

public static void sleep(long millis)

1.作用:使当前线程暂时停止执行指定毫秒数。暂停执行期间线程处于等待状态(WAITING/TIMED_WAITING)。
2.注意:sleep()Thread类中的静态方法,必须在线程的逻辑执行体中调用,例如main(){},run(){}call(){}sleep(long millis)会抛InterruptedException
3.扩展:sleep()的另外一个版本:public static void sleep(long millis,int nanos)。在sleep(long millis)的基础上增加毫秒和纳秒精度,nanos的范围为0-999999sleep(long millis,int nanos)会抛InterruptedException
4.示例:

public class Main {
    public static void main(String[] args) {
        Thread.currentThread().setName("MainThread");
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+" output "+i);
            if(i%10==0){
                try{
                    Thread.sleep(500);
                } catch(Exception e) {
                    System.out.println("some error occur");
                }
            }
        }
    }
}

public final boolean isAlive()

 判断线程是否仍然存活。

public final void setPriority(int newPriority)

1.作用:设置线程的调度优先级。
2.注意:优先级更高的线程不一定先执行,只是拥有更高的CPU调度概率
3.扩展:线程的优先级范围为1-10Thread类中定义了三个常量标识具有代表性的优先级:

//最低优先级为1
public static final int MIN_PRIORITY  = 1;
//中间优先级为5(默认优先级)
public static final int NORM_PRIORITY = 5;
//最高优先级为10
public static final int MAX_PRIORITY = 10;

public final int getPriority()

1.作用:获取线程优先级。
2.示例:

Call.java

public class Call implements Callable<String> {
    @Override
    public String call() {
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+" output "+i);
        }
        return "this is a test";
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new FutureTask<String>(new Call()));
        
        thread.setName("CallThread");
        Thread.currentThread().setName("MainThread");
        
        thread.setPriority(Thread.MAX_PRIORITY);
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
        
        thread.start();
        
        for(int i=0;i<100;i++){
            if(i==0){
                System.out.println("Priority of "
                                   +thread.getName()
                                   +" is "+thread.getPriority());
                System.out.println("Priority of "
                                   +Thread.currentThread().getName()
                                   +" is "+Thread.currentThread().getPriority());
            }
            System.out.println(Thread.currentThread().getName()+" output "+i);
        }
    }
}

线程的生命周期

 一个线程的生命周期大致为:createrun(pause)terminated
 从线程Thread对象的创建到调用start()之间为线程的创建;调用start()后线程进入运行状态;线程运行期间(可能)处于阻塞或者等待状态而暂停运行;线程执行完毕或者由于异常终止后线程生命终止

Thread类中定义了线程的状态标识:

public enum State {
    /*就绪(线程创建到调用start()之间)*/
    NEW,
    /*运行(调用start()后)*/
    RUNNABLE,
    /*阻塞(线程未获得而等待获得同步锁)*/
    BLOCKED,
    /*等待(线程已获得同步锁,但需要等待其它线程进行某些操作)*/
    WAITING,
    /*限时等待(线程已获得同步锁,但需要限时等待其它线程进行某些操作)*/
    TIMED_WAITING,
    /*终止(线程执行完毕或由于异常终止)*/
    TERMINATED;
}

可以使用Thread对象的getState()(定义为public State getState(){})获取当前的线程状态。
线程阻塞线程等待的区别:
线程阻塞是由于线程未获得同步锁而不能操作共享资源,只有其它线程中对于共享资源的操作结束并且获得同步锁之后,线程才能操作共享资源,在此期间线程处于阻塞状态
线程等待是由于线程运行时需要等待其它线程的某些操作完成,或者需要减缓线程的运行速度而进行的主动等待,此时线程已经获得同步锁

线程同步

线程安全

多个线程操作共享数据时可能会出现线程安全问题。例如线程a需要根据共享标志变量flagtrue/false进行不同的操作,线程a判断完毕后开始执行相应的操作,此时线程b改变flag的值,而线程a依然根据flag被线程b更改之前的值进行操作,最终将导致线程a操作与判断不一致的问题。实例:

DrawMoney.java

public class DrawMoney implements Callable<String> {
    //提款人
    private String user;
    //余额
    private static int balance = 5000;
    //本次提款数额
    private int howMuch;
    
    public DrawMoney(String user,int howMuch) {
        this.user = user;
        this.howMuch = howMuch;
    }
    
    public String call() {
        if((balance-this.howMuch) >= 0) {
            balance -= this.howMuch;
            System.out.println(this.user+"本次提款:"+this.howMuch+" 元");
        }
        else {
            System.out.println("抱歉,您的账户余额不足!");
        }
        return "this is a test.";
    }
    
    public static int getBalance() {
        return balance;
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        //妻子将提款4000元
        Thread wife = new Thread(new FutureTask<String>(new DrawMoney("妻子",4000)));
        //丈夫将提款2000元
        Thread husb = new Thread(new FutureTask<String>(new DrawMoney("丈夫",2000)));
        
        //丈夫和妻子同时开始提款
        wife.start();
        husb.start();
        
        //等待提款操作完成
        while (wife.isAlive()||husb.isAlive());
        
        System.out.println("余额:"+DrawMoney.getBalance()+" 元");
    }
}

 妻子提款和丈夫提款是两个独立运行的线程,可能会出现一种极端情况,例如:妻子提款完成后余额管理系统未及时更新余额,同时丈夫开始提款。由于余额未及时更新,丈夫提款时余额管理系统仍然判断余额足够,当丈夫提款完成后,余额已为负值。
 多个线程操作共享数据时,尽管极端情况发生的概率很小,但在很多时候,线程安全问题会带来极大的隐患。因此多个线程操作共享数据时必须考虑线程安全问题。

同步代码块

/**
*monitor:同步监视器(任何对象)
*/
synchronized(monitor) {
    //需要被同步的代码块
}

 由**(synchronized关键字)+(同步监视器)+(需要被同步的代码块)构成的代码块称为同步代码块**。
 同步监视器的使用:同步监视器可以是任何类的对象(包括任何类的Class对象)操作共享资源的同步代码块必须使用同一个同步监视器
 对于使用相同同步监视器的同步代码块,任何时刻都只有一个同步代码块拥有同步监视器,其它线程中未获得同步监视器的同步代码块将导致线程处于阻塞状态。同步代码块执行完毕后自动释放同步监视器,其它同步代码块开始竞争同步监视器。
对于提款的改进:

DrawMoney.java

public class DrawMoney implements Callable<String> {
    //提款人
    private String user;
    //余额
    private static int balance = 5000;
    //本次提款数额
    private int howMuch;
    //使用当前类的Class对象作为同步监视器
    private static final Class<?> monitor = DrawMoney.class;
    
    public DrawMoney(String user,int howMuch) {
        this.user = user;
        this.howMuch = howMuch;
    }
    
    public String call() {
        synchronized(monitor) {
            if((balance-this.howMuch) >= 0) {
            	balance -= this.howMuch;
            	System.out.println(this.user+"本次提款:"+this.howMuch+" 元");
        	}
        	else {
            	System.out.println("抱歉,您的账户余额不足!");
       	 	}
        }
        return "this is a test.";
    }
    
    public static int getBalance() {
        return balance;
    }
}

同步方法

 使用synchronized修饰的方法为同步方法。将需要同步的操作逻辑置于同步方法中,作用与同步代码块相同。同步方法的同步监视器为this,没有显式声明的同步监视器。
 同步方法的局限在于:使用不同对象创建线程时同步监视器对应不同的对象,从而导致同步监视器不一致。解决方法:将同步方法声明为静态方法此时同步监视器为当前类的Class对象。

private static synchronized void synMethod() {}

对于提款的改进:

DrawMoney.java

public class DrawMoney implements Callable<String> {
    //提款人
    private String user;
    //余额
    private static int balance = 5000;
    //本次提款数额
    private int howMuch;
    
    public DrawMoney(String user,int howMuch) {
        this.user = user;
        this.howMuch = howMuch;
    }
    //将同步方法声明为 静态方法
    private static synchronized void draw(String user,int howMuch) {
        if((balance-howMuch) >= 0 ) {
            balance -= howMuch;
            System.out.println(user+"本次提款:"+howMuch+" 元");
        }
        else {
            System.out.println("抱歉,您的账户余额不足!");
        }
    }
    
    public String call() {
        //在线程逻辑执行体中调用同步方法
        draw(this.user,this.howMuch);
        return "this is a test.";
    }
    
    public static int getBalance() {
        return balance;
    }
}

死锁

 线程不释放同步监视器的情况称为死锁。线程中出现死循环或者线程之间互相等待对方释放同步监视器的情况都会导致死锁。死锁不会导致程序运行出错或者抛异常,而是导致线程永久处于阻塞状态。实例:

Main.java

public class Main {
    
    private static final String str_1 = "give me the str_2";
    private static final String str_2 = "give me the str_1";
    
    public static void main(String[] args) {
        //线程1
        new Thread(new FutureTask<String>(new Callable<String>() {
            @Override
            public String call() {
                synchronized(str_1) {
                    System.out.println(str_1);
                    synchronized(str_2) {
                        System.out.println(str_2);
                    }
                }
                return Thread.currentThread().getName();
            }
        })).start();
        //线程2
        new Thread(new FutureTask<String>(new Callable<String>() {
            @Override
            public String call() {
                synchronized(str_2) {
                    System.out.println(str_2);
                    synchronized(str_1) {
                        System.out.println(str_1);
                    }
                }
                return Thread.currentThread().getName();
            }
        })).start();
    }
}

 实例中出现了同步代码块的嵌套:线程1中的外层监视器为str_1,内层为str_2;线程2的外层监视器为str_2,内层为str_1。两个线程的内层监视器分别对应对方的外层监视器,因此线程1开始运行后必须获得str_2才能运行完毕,线程2开始运行后必须获得str_1才能运行完毕。
 若线程1和线程2运行时均未获得各自的内层监视器,两个线程将一直等待对方运行完毕后释放外层监视器而处于僵持局面,线程将永久阻塞。
 实际开发中应当慎重选择同步监视器,避免出现死锁。

线程通信

线程影响其它线程运行状态的操作称为线程通信。实现线程通信的方法:

wait():
使当前线程阻塞释放同步监视器,直到被其它线程唤醒并获得同步监视器才继续执行。线程被唤醒之前不能参与同步监视器的竞争
wait()必须在同步代码块(或同步方法)中由对应的同步监视器调用
wait()会抛InterruptedException
wait(long timeoutMillis):
使当前线程阻塞指定毫秒数并释放同步监视器,直到被其它线程唤醒并获得同步监视器或到达设定时间才继续执行。线程被唤醒之前不能参与同步监视器的竞争
wait(long timeoutMillis)必须在同步代码块(或同步方法)中由对应的同步监视器调用
wait(long timeoutMillis)会抛InterruptedException
timeoutMillis设置为0时作用与wait()相同。
wait(long timeoutMillis,int nanos):
 在wait(long timeoutMillis)基础上增加纳秒精度。timeoutMillisnanos均为0时作用与wait()相同。
notify():
 唤醒一个在此同步监视器(notify()的调用者)上等待(被wait()阻塞)的线程,使其参与同步监视器的竞争。如果有多条线程等待,notify()随机唤醒其中一个线程。
被唤醒的线程将继续阻塞,直到竞争获得同步监视器才继续执行
notify()必须在同步代码块(或同步方法)中由对应的同步监视器调用
notifyAll():
 唤醒所有在此同步监视器等待的线程,使它们参与同步监视器的竞争
被唤醒的线程将继续阻塞,直到竞争获得同步监视器才继续执行
notifyAll()必须在同步代码块(或同步方法)中由对应的同步监视器调用

注意:

sleep()wait()的异同:sleep()wait()都能使线程阻塞或限时阻塞。sleep()使线程阻塞时,线程仍然持有同步监视器wait()使线程阻塞时,线程释放同步监视器,并且线程被唤醒之前不能参与同步监视器的竞争sleep()可以在线程逻辑执行体中的任何位置调用;wait()只能在同步代码块或同步方法中调用
wait(),wait(long timeoutMillis),wait(long timeoutMillis,int nanos),notify()notifyAll()是定义在Object类中的方法,它们只能在同步代码块或同步方法中由对应的同步监视器调用

实例:

Main.java

public class Main {
    
    //设备1
    private static final Object equipment_1 = new Object();
    //设备2
    private static final Object equipment_2 = new Object();
    //当前设备
    private static Object currentEquipment = equipment_2;
    //零件1加工进度
    private static int progress_1 = 0;
    //零件2加工进度
    private static int progress_2 = 0;
    
    public static void main(String[] args) {
        
        //零件1加工线程
        new Thread(new FutureTask<String>(new Callable<String>() {
            @Override
            public String call() {
                for(int i = 1;i <= 100;i++) {
                    synchronized(equipment_1) {
                        progress_1++;
                        System.out.println("零件1加工进度:"+progress_1+"%");
                        if(progress_2 == 50) {
                            try{
                                System.out.println("零件1暂停加工");
                                equipment_1.wait();
                                System.out.println("零件1继续加工");
                            } catch(Exception e) {
                                System.out.println("some error occur.");
                            }
                        }
                    }
                }
                System.out.println("零件1加工完成");
                return "this is a test.";
            }
        })).start();
        
        //零件2加工线程
        new Thread(new FutureTask<String>(new Callable<String>() {
            @Override
            public String call() {
                for(int i = 1;i <= 100;i++) {
                    synchronized(currentEquipment) {
                        progress_2++;
                        System.out.println("零件2加工进度:"+progress_2+"%");
                        if(progress_2 == 50) {
                            System.out.println("零件2占用设备1");
                            currentEquipment = equipment_1;
                        }
                        else if(progress_2 == 80) {
                            currentEquipment.notify();
                            currentEquipment = equipment_2;
                            System.out.println("零件2退出设备1");
                        } 
                    }
                }
                System.out.println("零件2加工完成");
                return "this is a test.";
            }
        })).start();
        
    }
}

 代码实现:零件1和零件2分别由设备1和设备2加工,对应各自的加工线程。通过加工进度(0-100%)表示零件的加工过程。零件2进度的51%-80%需要占用设备1加工。零件1线程同步监视器为equipment_1,零件2线程同步监视器为可更换的currentEquipment,起始为equipment_2
通过换锁实现零件2对设备1的申请和占用:零件2加工到50%时,零件1线程必须准确判断零件2的加工进度,否则零件2将继续在设备2中加工,出现错误工序。具体的做法是使零件2线程加工到50%时暂停等待零件1线程的响应。
 代码中暂停零件2线程的做法:零件2加工到50%时,更换线程同步监视器为equipment_1,由于此时零件1线程仍然持有equipment_1,零件2线程进行下一步加工时将被阻塞暂停。在零件1线程响应之前,零件2的加工进度停留在50%,从而使零件1线程能够及时准确地判断零件2的加工进度。
 零件1线程判断零件2加工进度为50%后,equipment_1调用wait()使零件1线程阻塞并释放equipment_1。之后零件2线程将获得equipment_1并开始运行。零件2线程进度为80%equipment_1调用notify()唤醒零件1线程并再次更换线程同步监视器为equipment_2。之后零件2线程将在equipment_2中继续执行直到加工完成,零件1线程获得equipment_1后继续运行直到加工完成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值