JAVA多线程基础

JAVA多线程基础

线程基础

对于计算机而言,每个任务称为一个进程,而一个进程至少有一个线程。
对于JVM来说,每个线程拥有自己的虚拟机栈、本地方法栈、程序计数器、线程之间共同使用堆、方法区。

线程的生命周期

  • NEW:当程序创建一个Thread对象时,该对象处于NEW状态。
  • RUNNABLE:Thread对象调用Start()方法,线程进入RUNNABLE状态,这个状态下线程具备执行资格,等待CPU调度。
  • RUNNING:一旦CPU调度中选中了处于RUNNABLE状态的线程,线程进入运行状态,此时开始执行真正的代码逻辑。在该状态下状态可能转变为TERMINATED(代码运行正常结束/意外出错)、BLOCKED(进行IO操作/wait、sleep方法/尝试获取锁资源)、RUNNABLE(CPU调度/调用yield方法)
  • BLOCKED:由于IO、wait方法、sleep方法、尝试获取锁的过程中,这些原因会使线程进入阻塞状态。
  • TERMINATED:线程最终状态,意味着线程生命周期的结束。

线程的三种启动方式

  1. 继承Thread类
    主要方式时通过继承的方式实现,单JAVA只允许单继承,这种方式有一定的局限性。
public class  mytest{
    public static void main(String[] args) {
        Thread  t1 =new myThread();
        Thread  t2 =new myThread();
        t1.start();
        t2.start();
    }

}
class myThread extends Thread{
    @Override
    public void run() {
        for (int i = 0;i<10;i++){
            System.out.println("线程启动啦");
        }
    }
}

  1. 实现Runnable接口
    java接口可以多实现,推荐以接口的方式创建线程。
public class  mytest{
    public static void main(String[] args) {
        Thread  t1 =new Thread(new myThread());
        Thread  t2 =new Thread(new myThread());
        t1.start();
        t2.start();
    }

}
class myThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0;i<10;i++){
            System.out.println("线程以Runnable启动啦");
        }
    }
}
  1. 实现Callable接口
    Callable和Runnable都是接口创建线程,但是Callable可以获取返回值,可以在特定场景下使用。
public class  mytest{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<myThread> f1= new FutureTask<myThread>(new myThread());
        FutureTask<myThread> f2= new FutureTask<myThread>(new myThread());
        Thread  t1 =new Thread(f1);
        Thread  t2 =new Thread(f2);
        t1.start();
        t2.start();
        System.out.println(f1.get());
        System.out.println(f2.get());
    }

}
class myThread implements Callable {
    @Override
    public Object call() throws Exception {
        for (int i = 0;i<10;i++){
            System.out.println("线程启动啦");
        }
        return 100;
    }
}

守护线程

守护线程也可以称为后台线程,当JVM没中没有非守护线程,JVM进程就会退出。
因此守护线程具备自动结束生命周期的特点,常用做执行一些后台任务。可以通过setDeamon()方法设置守护线程。

mythread.setDaemon(true);

sleep()方法

sleep方法会使当前线程进入指定的毫秒数的休眠,虽然指定了时间,但是最终还是以系统定时器和调度器为准。休眠有一个重要特征就是不会释放锁。
sleep还可以通过TimeUnit来调用,他提供了一系列的封装。

        TimeUnit.DAYS.sleep(1);//休眠一天
        TimeUnit.HOURS.sleep(3);//休眠三小时
        TimeUnit.MINUTES.sleep(25);//休眠25分钟
        TimeUnit.SECONDS.sleep(15);//休眠15秒
        TimeUnit.MILLISECONDS.sleep(88);//休眠88毫秒

yield()方法

yield方法主要是提醒调度器我愿意放弃当前的CPU资源,如果CPU资源并不紧张的话,会忽略这种提醒。

优先级

        t1.setPriority(6);//设置优先级 范围1-10
        t1.getPriority();//获取优先级

进程有优先级,线程也有优先级,理论上优先级较高的线程会优先被CPU调度。在CPU比较忙的时候,高优先级的线程会得到更多的CPU时间片,而闲时优先级高低不会有任何作用。不要依赖优先级去完成代码的业务需求。一般情况下默认优先级是5.

线程ID与线程名

可以通过Thread.currentThread()方法获取当前线程,并通过getId()的方法获取线程的唯一ID,线程的ID在整个JVM中唯一并且是从0开始递增的。getName()方法可以获取线程名。

        System.out.println(Thread.currentThread().getId());
        System.out.println(Thread.currentThread().getName());

线程interrupt

        Thread.currentThread().interrupt();
        Thread.currentThread().isInterrupted();
        Thread.interrupted();

interrupt()可以中断线程,实质上是设置中断标志。对阻塞状态的线程调用interrupt()方法可以打断阻塞,如wait()、sleep()、join()等,线程会清除中断标志,中断的线程会抛出InterruptedException的异常。
isInterrupted()方法可以判断当前线程是否被中断,这仅仅只是对中断标识的判断。
interrupted()也是判断线程是否被中断,但该方法会清除线程中断标识。

线程Join

join方法有三种方式

        t1.join();
        t1.join(100);//执行毫秒
        t1.join(100,200);//执行毫秒、纳秒

在当前线程中调用join方法,可以加入另一线程,当前线程进入阻塞状态,等待另一线程的完成,或者指定时间结束。

synchronized

synchronized关键字是一种同步锁机制,可以确保共享变量的互斥访问。也就是说synchronized修饰的代码区域,同一时间只允许获得锁对象的唯一线程进入。synchronized有以下几种用法:

用法被锁对象说明
修饰静态方法类对象public static synchronized void test(){ 业务处理 }
修饰非静态方法实例对象public synchronized void test(){ 业务处理 }
代码块锁定当前对象实例对象synchronized (this){ 业务处理 }
代码块锁定类对象类对象synchronized (mytest.class){ 业务处理 }
代码块锁定任意实例对象实例对象synchronized (obj){ 业务处理 }

synchronized关键字包括了monitor enter和monitor exit两个JVM指令。

  • monitor enter:每个对象都与一个monitor关联,一个monitor的锁同一时间只会被一个对象获取到。一个线程尝试获得monitor时,如果moniter的计数器为0,则对计数器加一,从此就获得了这个monitor,如果发生了重入,则monitor的计数器再次累加。如果尝试获得monitor时,moniter已经被其他线程拥有,则陷入阻塞状态等待moniter的计数器变为0再尝试获取monitor。
  • minitor exit:释放monitor,主要过程就是对monitor的计数器减一,如果计数器归零则说明该线程不再拥有这个monitor,其他线程可以尝试获取它。
    synchronized在JDK1.6之后做了一个优化操作
    对象头种Mark Word部分有锁标志位,标志了四种锁的状态
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uYrd6C1n-1618133701110)(MarkWord.png)]
    锁升级过程可以简单描述为:无锁状态–>偏向锁–>轻量级锁–>重量级锁
偏向锁

偏向锁的锁标志位为01,这点与无锁状态的锁标志位是相同的,但是偏向锁会在对象头记录当前获取到这把偏向锁的线程id。所谓偏向锁就是说它会偏向于第一个获得它的线程。
当线程执行到同步代码块时会判断对象头的线程id是否等于当前线程ID,如果是则执行接下来的代码,如果不是则尝试通过CAS获取偏向锁,如果存在竞争,就会对锁进行膨胀,转为轻量级锁。
偏向锁失效时需要撤销锁,锁撤销的开销比较大,某些情况下我们可以默认关闭偏向锁。

轻量级锁

轻量级锁的标志位为00,锁升级过程主要包括以下几步:

  1. 栈桢中创建锁记录LockRecord。
  2. 将锁对象的对象头中的MarkWord复制到锁记录中。
  3. 将锁记录中的指针指向锁对象。
  4. 将锁对象的对象头的MarkWord替换为指向锁记录的指针。
    轻量级锁主要通过自旋锁的方式实现,当自选次数超过一定次数就会膨胀成重量级锁。自旋锁主要的思路就是在竞争锁时,其他线程以空循环的方式原地自旋等待,而非进入阻塞状态。自旋锁的好处是避免了上下文切换的开销,在面对保持锁时间比较短的情况下,有远高于重量级锁的效率,但是避免长时间忙等待消耗CPU资源,因此设置了自选次数。
重量级锁

重量级锁的锁标志位为10,重量级锁的实现方式就是通过上文中提到monitor指令完成的,重量级锁也称为互斥锁。
重量级锁在阻塞或唤醒线程的过程中,会涉及到用户态和内核态的转换,这种转换状态需要很多时间,因此开销很大

通过以上锁升级的过程可以看出,synchronized在低竞争的情况下也可以通过偏向锁和轻量级锁获得很好的效率,但是在竞争激烈的地方,或许应该考虑下是否直接使用重量级锁,这样可以节省锁升级过程中的开销。

死锁

产生死锁的必要条件:

  1. 互斥条件:进程要求对所分配的资源进行互斥控制。
  2. 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺。
  4. 环路等待条件:在发生死锁时,必然存在一个的环形链。

同理,预防死锁的主要思路就是破坏上述条件。

  1. 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
  2. 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
  3. 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
  4. 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

wait和notify

wait方法会让当前线程进入阻塞状态,只有当其他线程调用notify和notifyall才能唤醒,或者是阻塞时间到了自动唤醒。wait方法必须在同步代码中使用,其会释放获得的monitor。
notify方法会唤醒由于wait进入阻塞的单个线程,唤醒的线程需要重新获得monitor才能继续执行。
wait和sleep的区别:

  • wait是object的方法,sleep是Thread的方法。
  • wait必须在同步方法中执行,sleep不需要。
  • wait会释放锁,而sleep不会
    wait set:线程调用了wait方法后会加入该对象monitor关联的wait set中,notify会弹出一个线程,notifyAll会弹出所有线程。

线程池

这里介绍四种常用的线程池,但不推荐使用。

名称创建方式说明
固定线程池Executors.newFixedThreadPool(10)以固定线程数量的方式创建一个线程池
缓存线程池Executors.newCachedThreadPool()一个可以增长线程数量的线程池,长时间闲置的线程会被回收
单线程线程池Executors.newSingleThreadExecutor()以单线程的形式创建线程池
计划线程池Executors.newScheduledThreadPool(10)一个可增长的线程池,可以执行延时任务和定时任务

这里介绍一下线程池的构造方法

public ThreadPoolExecutor(    int corePoolSize,                         //核心线程数
                              int maximumPoolSize,                      //最大线程数
                              long keepAliveTime,                       //超过核心数的线程的最长空闲存活时间
                              TimeUnit unit,                            //前一个参数的单位
                              BlockingQueue<Runnable> workQueue,        //保存任务的队列
                              ThreadFactory threadFactory,              //线程工厂,创建线程时需要执行的工厂
                              RejectedExecutionHandler handler)         //拒绝策略,队列满时执行的策略

之所以不推荐使用上述前三个线程池,主要原因前三个线程池在构造线程池时,传入的阻塞队列是基于链表的LinkedBlockingQueue(),这是一个无边界队列,有触发OOM的安全隐患,因此最好是通过线程池构造函数自己创建需要的线程池。

volatile

volatile关键字主要有两个作用

  • 保证变量的可见性,保证该变量在一个线程修改后,马上可以被其他线程看到,避免因为JMM内存模型导致的修改工作副本而对其他线程不可见的情况。
  • 禁止指令重排,jvm运行编译器和处理器进行指令重排序,但是要求不能影响结果,这种情况在多线程下有可能会出现问题,因此可以通过volatile关键字禁止指令重排。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值