JAVA多线程(第二期)

Thread的应用

1.如何启动一个线程

        Thread类使用start方法启动一个线程,但对于一个线程,只能调用一次start方法,否则会发生异常。

2.start和run的区别(经典面试题)

        启动线程时,如果用run,那么只是执行线程的run方法,并不是多个线程同时执行,如果run方法有循环的话,那么就是一直循环;如果用start,可以同时执行其他线程。

3.终止一个线程

        通常当一个线程的run方法执行完成后,线程就终止了,有时为了让线程提前终止,可以引入一个标志位,可以在另一个线程中在合适时机修改标志位。但设置标志位时要在main类外设置一个static的标志位。因为lambda要获取final型的变量。只有设置在main外才可以。

        Thread也自带标志量,通过Thread.currentThread.isInterrupted()来调用,默认为false,可以调用t.interrupt()来唤醒其他线程。

4.java为什么没有引入硬性终止线程

        因为线程一旦强行终止,会产生许多临时文件,如果线程在打开图片或访问文件时,强行终止,会导致图片/文件产生不可预知的错误。

5.线程执行的顺序

        多个线程的执行顺序是不确定的,但引入join就可以确定顺序。当一个线程调用join后,其他线程要等待该线程执行完成之后才可以运行。在使用join时,要注意抛出InterruptedException异常。

Thread t = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                System.out.println("线程正在执行");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        t.join();

        System.out.println("这是主线程,在t之后执行");

在这个例子中,main线程要等待t线程执行完成后才会执行。

6.join的不同版本

        1)死等版本,join(),不科学,容易造成死锁现象。

        2)带有超时时间的版本,有一个等待上限,join(long millis),也可以使用interupt方法,将等待运行join线程的线程提前唤醒。

6.获取线程的引用

        Thread.currentThread()获取到当前线程的引用(Thread的引用)

如果是继承Thread,直接使用this那到线程实例/

如果是Runnable或者lambda的方式,this就无能为力了,此时this已经不再指向Thread对象了,只能使用Thread.currentThread(),

 Thread t = new Thread(()->{
            Thread t1 = Thread.currentThread();
            System.out.println(t1.getName());
        });
        t.start();

线程的状态

1.NEW:Thread对象创建好了,但是还没有调用start方法在系统内创建线程。

2.TERMINATED: Thread对象仍然存在,但是系统内部的线程已经执行完毕了。

3.RUNNABLE:就绪状态,表示这个线程正在cpu上执行,或者准备就绪随时可以去cpu上执行。

4.TIMED_WAITING: 指定时间的阻塞,就在到达一定时间后自动解除阻塞,使用sleep会进入这个状态,使用带有超时时间的join也会。

5.WAITING:不带时间的阻塞,必须要满足一定条件才会接触阻塞。join和wait都会进入WAITINNG。

6.BLOCKED:由于锁竞争而引起的阻塞。

NEW---调用start--->RUNNABLE--sleep--->TIMED_WITING;---join/wait---->WAITING;----run方法执行完毕---->TERMINATED

可以通过jconsole来查看线程的状态。

7.线程安全(较难)

        引入多线程,是为了实现“并发编程”,但是实现“并发编程”,并不能仅仅依靠多线程,后世的一些其他编程语言,引入了封装层次更高的并发编程模型,往往会比java多线程,要更方便,更简单,更不易出错。

        典型代表:erlang: actor模型

                        go:  csp模型

                        js: 基于定时器/异步模型..........

某个代码,无论是在单个线程下执行,还是多线程下执行,都不会产生bug,这个情况就称为“线程安全”,但某个线程在单个线程下执行安全,在多线程下执行产生bug,就称为“线程不安全”。

 Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });
        t1.start();;
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count= "+count);

第一次运行:count= 7053;

第二次运行:count= 6052;

很明显,这个代码有问题,结果应该是10000,但是每次结果都不同,这就是“不安全线程”。

为什么?因为count++其实是由cpu三个指令构成的。

1)load 从内存中读取数据到cpu的寄存器。

2)add 把寄存器中的值+1.

3)save 将寄存器中的值写回到内存中。

如果是一个线程执行上述三个指令,当然没问题,但是如果是两个线程,并发执行上述操作,此时就会存在变数,因为线程间的调度是不确定的!这三个指令会无规律的执行,导致答案错误。

1.根本原因:操作系统上的线程是“抢占式执行” ”随机调度“--->给线程的执行顺序带来了许多变数。

2.代码结构有问题:代码中多个线程,同时修改同一个变量。

3.直接原因:上述多线程修改操作,本身不是“原子操作”;

其他会导致线程不安全的原因:

4.内存可见性问题。

5.指令重排序问题。

解决方法:

针对1,可以通过修改操作系统内核来改变,但是意义不大/

针对2,有时可以通过修改代码来解决,有时则不可以。

针对3,可以通过特殊方法,将三个指令打包到一起,成为“整体”,其中“加锁”就是一种方法,“锁”有“互斥”和“排他”的特性。在Java中,有好几种方式加锁,我们主要使用“synchornized”关键字来加锁,读作“xing ke rou nai zi de”。

要加锁时,需要准备好“锁对象”,加锁解锁都是依托于锁对象来展开的,如果一个线程,针对一个对象加上锁后,其他线程,也尝试对这个对象加锁,就会产生阻塞(BLOCKED),一直阻塞到前一个线程释放锁为止。在Java中任何一个对象都可以是锁对象。对于下面的例子,t1线程的count++就变成了一个原子操作,每次执行count++时都会加锁,这时t2线程就竞争不到锁,就处于阻塞状态,直到t1线程的count++执行完毕。

加锁后,好像变成了串行执行,但加锁虽然会影响到效率,但执行速度还是要远远大于串行执行的,因为加锁只是锁住了某几个指令,大部分指令还是可以并发执行的,

Object lack = new Object();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                synchronized (lack){
                    count++;
                }

            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                synchronized (lack){
                    count++;
                }
            }
        });
        t1.start();;
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count= "+count);

易错点:

1)如果一个线程加锁,另一个不加锁,仍然会出现“线程安全”问题。

2)如果两个线程,针对不同的对象加锁,也会出现线程安全问题。

3)针对加锁操作的一些混淆的理解

class Test{
    public  int count=0;
    public void add(){
        synchronized (this){
            count++;
        }
    }
}
public class Thread16 {
    public static void main(String[]args) throws InterruptedException {
        Test t = new Test();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
               t.add();
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                t.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count= "+t.count);
    }
}

这样写代码,并没有线程安全问题,在t1线程中this指的是t,t2线程中this也是指的t,这样也会存在锁竞争,线程是安全的。

如果把锁对象换成Test.class也是ok的,

public void add(){
        synchronized (Test.class){
            count++;
        }
    }

每个类都有自己的类对象,通过类名.class获取,类对象里包含了类的各种信息,比如有什么属性,每个属性(变量)的名字,方法的名字,参数........

一个类的类对象是唯一的,也就是说锁是唯一的,这时线程也是安全的。

类对象也是反射机制的依据。

如果是synchronized(this)也可以等价写作把synchronized加到方法上。

 public void add(){
        synchronized (this){
            count++;
        }
    }
}
synchronized public void add(){
            count++;
    }

这两种方法等价。

如果synchronized写到static方法上,相当于给类对象加锁。

public static void fun(){
        synchronized (Test.class){
            count++;
        }
    }
synchronized public static void fun(){
        count++;
    }

这两种方式等价。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值