浅学多线程(1)


 相对于传统单线程,多线程能够在操作系统多核配置的基础上更好的利用服务器的多个cpu资源,使程序运行更加高效。Java通过提供对多线程的支持来在一个进程内并发执行多个线程,每个线程都并行执行不同任务,以满足编写高效程序的要求。

小概念

1、并行和并发的区别:
 并行是指多个事件同一时刻发生;并发是指多个事件在同一时间间隔发生。并行宏微观都同时;并发宏观同时,微观顺序执行
 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
2、线程和进程的区别:
 进程:是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。
线程:是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程的多个线程之间可以并发执行。

多线程

1、线程创建的几种方式

1、继承Thread类
 Thread类实现了Runnnable接口并定义了操作线程的一些方法。具体实现为创建一个类并继承Thread类,实例化线程对象并调用start方法启动线程。start方法是一个native方法,通过在操作系统上启动一个新线程,并最终执行run方法实现具体逻辑。

class MyThread extends Thread{
    public MyThread(String name){
        super(name);
    }
    public void run(){
        for (int i = 1 ; i <= 10 ; i++)
            System.out.println(getName() + " is running " + i );
    }
}
public class ThreadTest {
    public static void main(String[] args) {
        MyThread mt1 = new MyThread("1");
        MyThread mt2 = new MyThread("2");
        mt1.start();
        mt2.start();
    }
}

2、实现Runnable接口
 Java类为单继承,但接口支持多继承。或者不打算重写Thread类的其他方法。

class PrintRunnable implements Runnable{
    int i=0;
    @Override
    public void run() {
        for (;i<10;i++)
        System.out.println(Thread.currentThread().getName()+" is running " + i);
    //Thread.currentThread()可以获取当前线程的引用
    //没有线程对象又需要获得线程信息时使用
    }
}

实例化PrintRunnable对象,创建线程对象并传入PrintRunnable实例。
Test1:

public static void main(String[] args) {
        PrintRunnable pr = new PrintRunnable();
        Thread t1  = new Thread(pr);
        t1.start();
        PrintRunnable pr1 = new PrintRunnable();
        Thread t2  = new Thread(pr1);
        t2.start();
    }

Test2:

public class Test {
    public static void main(String[] args) {
        PrintRunnable pr = new PrintRunnable();
        Thread t1  = new Thread(pr);
        t1.start();
//        PrintRunnable pr1 = new PrintRunnable();
        Thread t2  = new Thread(pr);
        t2.start();
    }
}

run方法中的代码可以被多个线程共享(Test1中两个线程各输出十条;Test2中使用同一个PrintRunnable对象,两个线程抢占式一共输出十条)
3、通过Callable<>实现有返回值的线程
 分析一下类关系

interface RunnableFuture extends Runnable {}
class FutureTask implements RunnableFuture{}
interface Callable{
    V call()
}
class MyCallable implements Callable{
//call就是子线程执行的主体
//类似但不同于run,call有返回值并且可以抛出异常
    public V call(){}
}
MyCallable callable = new MyCallable();
FutureTask task = new FutureTask(callable);
new Thread(task).start

 Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。

通过实现Callable接口创建MyCallable线程

public class MyCallable implements Callable<String> {
//    private String name;
//    public MyCallable(String name){
//        this.name = name;
//    }
    @Override
    public String call() throws Exception {
        System.out.println("子线程id=" + Thread.currentThread().getId() + "子线程名为" + Thread.currentThread().getName());
        return Thread.currentThread().getName();
    }
}

测试类:

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable mt = new MyCallable();
        FutureTask<String> task = new FutureTask<>(mt);
        Thread t1 = new Thread(task);
        t1.start();
        System.out.println(task.get());
    }
}

4、匿名内部类继承Thread

 Thread t = new Thread(name) {
        public void run() {
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i + ";");
            }
        }
    };

5、匿名内部类创建 Runnable 子类对象

Thread th3 = new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i=0;i<5;i++){
                    System.out.println("分支线程-->" + i);
                }
            }
        });

6、基于线程池创建(后续新开篇详解)
 线程是非常宝贵的计算资源,频繁的创建销毁非常浪费资源,可以使用缓存策略并使用线程池来创建线程。

public class ThreadPool {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        for (int i=0;i<10;i++){
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " is running");
                }
            });
        }
    }
}

2、线程的生命周期

 线程的生命周期分为新建(New),就绪(Runnable),运行(Running),阻塞(Blocked)和死亡(Dead)这5种状态。在系统运行过程中不断有线程创建和清理,线程在排队获取共享资源或者锁时将被阻塞,因此运行中的线程就会在就绪、阻塞、运行之间来回切换。
线程生命周期
其流程如下所示:
①、调用new方法新建一个线程,此时就处于新建状态。
②、调用start方法,此时线程进入就绪状态。
③、就绪状态的线程等待获取cpu资源,获取到cpu资源后执行run方法进入运行状态。
④、正在运行的线程调用yield方法或失去处理器资源后再次进入就绪状态。
⑤、正在运行的线程执行sleep方法、I/O阻塞、等待同步锁、等待通知、调用suspend方 法等操作后,会挂起并进入阻塞状态,进入Block池。
⑥、阻塞状态的线程由于sleep时间已到、I/O方法返回、获得同步锁、收到通知、调用 resume方法等,会再次进入就绪状态,等待cpu时间片轮询,获取cpu资源后进入运行状态。
⑦、处于运行状态的线程,在调用run方法或call方法正常执行完成、调用stop方法、程序执行错误导致异常退出时,进入死亡状态。
1、New:为线程分配内存并初始化其成员变量的值。
2、Runnable:JVM完成方法调用栈和程序计数器的创建,等待该线程的调度和运行。
3、Running:执行run中的逻辑代码。
4、Blocked:主动或被动放弃cpu使用权并暂停运行。阻塞状态分为 等待阻塞,同步阻塞,其他阻塞三种。

  • 等待阻塞:调用wait方法时,JVM会把该线程放入等待队列(Waiting Queue)。
  • 同步阻塞:运行状态的线程尝试获取正在被其他线程占用的对象同步锁时,JVM会把该线程放入锁池(Lock Pool)中。
  • 其他阻塞:执行Thread.sleep(long ms)、Thread.join()或是发出I/O请求时~。

5、Dead:三种方式:

  • 正常结束:run或call方法执行完成。
  • 异常退出:运行中的线程抛出一个Error或未捕获的Exception,异常退出。
  • 手动结束:调用线程对象的stop方法手动结束。该方式会瞬间释放线程占用的同步对象锁,导致锁混乱和死锁,不推荐

2*、线程的六种状态

状态名称说明
NEW初始状态,线程被构建,但未调用start()方法
RUNNABLE运行状态,java将就绪和运行笼统称为运行中
BLOCKED阻塞状态,表示线程阻塞和锁
WAITING等待状态,要等待其他线程通知或中断
TIMED_WAITING超时等待状态,不同于WAITING,可以在指定时间自行返回
TERMINATED终止状态,表示当前线程已执行完毕,内核中的线程已经结束了,但是Thread对象还在(需要GC回收)

isAlive表示线程存活,除了NEW和TERMINATED之外其余状态都是isAlive。

线程转换

3、线程的基本方法

 线程相关的基本方法有wait、notify、notifyAll、sleep、join、yield等。
1、线程等待wait:调用wait方法会进入WAITING状态,只有等到其他线程的通知或被中断后才会返回。在调用wait方法后会释放对象的锁,因此wait方法一般用于同步方法或同步代码块中。
2、线程睡眠sleep:调用sleep会导致当前线程休眠。与wait不同的是,sleep不会释放当前占有的锁,会导致线程进入TIMED-WAITING状态,而wait会进入WAITING状态。
3、线程让步yield:调用yield方法会使当前线程让出(释放)cpu执行时间片,与其他线程一起重新竞争cpu时间片。在一般情况下,优先级高的线程更有可能竞争到cpu时间片,但不是绝对的,有的操作系统对线程优先级并不敏感。
4、线程中断interrupt:interrupt方法用于向线程发出一个终止通知信号,会影响该线程内部的一个中断标识位,线程本身并不会因此改变状态。状态的具体变化要等待接收到中断标识的程序的最终处理结果判定。
 对interrupt方法需要注意以下四点:

  • 调用interrupt方法并不会中断一个正在运行的线程,也就是Running状态的线程并不会因为中断而终止,仅改变了内部维护的中断标识位。
  • 若因为调用sleep方法而使线程处于TIMED-WAITING状态,此时调用interrupt方法会抛出InterruptedException,使线程提前结束TIMED-WAITING状态。
  • 许多声明抛出InterruptedException的方法如Thread.sleep(long ms),在抛出异常前都会清除中断标识位,所以在抛出异常后调用isInterrupted方法会返回false。
  • 中断状态是线程固有的一个标识位,可以通过此标识位安全终止线程。比如先调用该线程的interrupt方法,然后在run方法中根据该线程的isInterrupted的返回状态值安全终止线程。
方法作用
interrupt()发起线程中断请求,但只是请求,并不会真的把线程给中断,实际上是把线程的中断标识设置为了true
isInterrupted()判断线程的中断标识是否为true
interrupted()判断线程的中断标识是否为true, 方法调用完之后,会将中断标识改为false

默认标识符为false,interrupt()没有返回值,只是将标识位改为true,如果该线程处于BLOCKING、WAITING、TIMED_WAITING状态或从这三种状态转换为RUNNABLE的过程中被interrupt中断,则会抛出InterruptException异常。如果本就处于RUNNABLE状态则无影响。
isInterrupted()返回值是boolean类型的返回值。

// interrupt()源码
    public void interrupt() {
        if (this != Thread.currentThread()) {
            checkAccess();

            // thread may be blocked in an I/O operation
            synchronized (blockerLock) {
                Interruptible b = blocker;
                if (b != null) {
                    interrupted = true;
                    interrupt0();  // inform VM of interrupt
                    b.interrupt(this);
                    return;
                }
            }
        }
        interrupted = true;
        // inform VM of interrupt
        interrupt0();
    }
//isInterrupted()源码
    public boolean isInterrupted() {
        return interrupted;
    }
//interrupted()源码
public static boolean interrupted() {
        Thread t = currentThread();
        boolean interrupted = t.interrupted;
        // We may have been interrupted the moment after we read the field,
        // so only clear the field if we saw that it was set and will return
        // true; otherwise we could lose an interrupt.
        if (interrupted) {
            t.interrupted = false;
            clearInterruptEvent();
        }
        return interrupted;
    }

interrupted()判断一下,如果标识位是true,就把标识位改为false。注意,上面两种方法是实例方法,要用Thread.currentThread().的方式调用,而interrupted()是静态方法,可以直接函数名调用。
5、线程加入join:join方法用于等待其他线程终止,如果在A线程中调用B线程的join方法,则A线程转为阻塞状态(其他阻塞状态),等B线程结束,A线程再由阻塞状态转为就绪状态。
6、线程唤醒notify:Object有一个notify方法,用于唤醒在此对象监视器上等待的一个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的。
 我们通常调用其中一个对象的wait方法在对象的监视器上等待着,直到当前线程放弃放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他线程竞争。类似的方法还有notifyAll,用于唤醒监视器上等待的所有线程。
7、后台守护线程setDaemon:setDaemon方法用于定义一个守护线程,也叫做"服务线程",该线程是后台线程,有一个特性,即为用户线程提供公共服务,在没有用户线程可服务时会自动离开。
 守护线程的优先级较低,用于为系统中的其他对象和线程提供服务。将一个用户线程设置为守护线程的方法是在线程创建之前用线程对象的setDaemon(true)来设置。
 在后台守护线程中定义的线程也是后台守护线程。后台守护线程是JVM级别的,比如经典的垃圾回收线程,它始终在低级别的状态下运行(回收完毕后也会自动离开),用于实时监控和管理系统中的可回收资源。
 守护线程是运行在后台的一种特殊线程,独立于控制终端并且周期性地执行某种任务或等待某些已发生的事件。即守护线程不依赖于终端,但依赖于JVM,与JVM同生共死。在JVM中所有线程都是守护线程时,JVM就可以退出了,反之不会。

4、sleep和wait方法的区别

  • sleep属于Thread类(静态方法),wait属于Object类(wait需要搭配synchronized使用)。
  • sleep暂停执行指定的时间,让出cpu给其他线程,但其监控状态一直保持,指定时间过后自动恢复运行状态(TIMED_WAITING状态,超时时间到了就恢复)。
  • sleep不会释放对象锁。
  • wait会放弃对象锁,进入等待此对象的等待锁池,只有此对象调用notify方法后,该线程才能进入对象锁池准备获取对象锁,并进入运行状态。

为什么wati() 方法和notify()方法需要synchronized一起使用?
 防止错过notify()发送的通知,造成永远阻塞等待。
 每一个对象都有一个监视器,监视器当中有有一个锁和一个阻塞队列和同步队列,因为wait()阻塞的线程放在阻塞队列中,因为竞争失败则放在同步队列当中,notify()则是把阻塞队列的线程放到同步队列当中去。

5、start和run方法的区别

  • start用于启动线程,真正实现了多线程运行。在调用了线程的start方法后,线程会在后台执行,无需等待run方法体的代码执行完毕,就可以继续执行下面的代码。
  • 在通过调用Thread类的start方法启动一个线程时,此线程处于就绪状态,并没有运行。
  • run也叫做线程体,包含要执行的线程的逻辑代码,在调用run方法后,线程就进入运行状态,开始运行run中的代码。run运行结束后线程终止,cpu再调度其他线程。

6、终止线程的4种方式

  1. 正常运行结束
    指线程体执行完成,线程自动结束。(run或call)
  2. 使用退出标志退出线程
    有些后台线程需要长时间运行,满足特定条件后才能触发关闭。此时可以自己设置一个boolean类型的标志,并通过设置该标志位true或false来控制while循环是否退出。
public class ThreadSafe extends Thread{
    public volatile boolean exit = false;
        public void run(){
            while (!exit){
                //执行业务逻辑代码
            }
        }
}	

 上面代码定义了一个退出标志exit,默认false。定义时使用了violate来使exit线程同步安全,也就是说同一时刻只能有一个线程修改exit的值,在exit为true时,while循环退出。

  1. 使用interrupt方法终止线程
     两种情况
    ①线程处于阻塞状态。如使用sleep、调用锁的wait或调用socket的receiver、accept方法时。此时调用interrupt会抛出InterruptedException异常。注:一定要先捕获异常再通过break跳出循环才能正常结束run方法。并不是调用interrupt就能结束
class SafeInterruptThread extends Thread {
    @Override
    public void run() {
        while(!Thread.currentThread().isInterrupted()) {
            try {
                System.out.println("运行中1");
                sleep(10);
                for (int i=0;i<10;i++){
                    System.out.println(Thread.currentThread().getName()+i);
                    if (i==9){
                        Thread.currentThread().interrupt();
                    }
                }
                System.out.println("运行中2");
                boolean s = Thread.currentThread().isInterrupted();
                System.out.println(s);
                if (Thread.currentThread().isInterrupted()){
                    System.out.println("hh");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
        }
        if (Thread.currentThread().isInterrupted()) {
            try {
                System.out.println("运行中3");
                sleep(100);
                System.out.println("运行中4");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("噶咯");
    }
}	
运行中1
Thread-00
Thread-01
Thread-02
Thread-03
Thread-04
Thread-05
Thread-06
Thread-07
Thread-08
Thread-09
运行中2
true
hh
运行中3
噶咯
java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep(Native Method)
	at com.thread.interrupt.SafeInterruptThread.run(SafeInterruptThread.java:30)

4.使用stop终止
 非常不安全捏,Thread.stop相当于断电关机,子线程会抛出ThreadDeatherror错误,并且释放子线程所有的锁。加锁的代码块一般用于保护数据的一致性,锁突然释放会导致锁资源不可控,可能出现数据不一致。其他线程使用这些被破坏的数据时有可能使程序运行错误。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值