多线程基础

1.线程如何优雅关闭

(1) 强制关闭
stop()和destory()函数可以强制的杀死线程,但线程中所使用的资源并不能正确的关闭。实际上这两个函数官网都是明确不建议使用的。
(2) 线程的分类
在Java中存在两种线程,一种是普通线程,一种是守护线程。通过thread.setDaemon(true)设置为守护线程。
当所有的非守护线程退出时,整个JVM进程就会退出。守护线程不影响整个JVM进程的退出。

import java.util.concurrent.TimeUnit;

public class Daemons {

    public static void main(String[] args) throws InterruptedException {
        Thread d = new Thread(new Daemon());
        d.setDaemon(true); //必须在启动线程前调用,设置为守护线程
        d.start();
        System.out.println("d.isDaemon() = " + d.isDaemon() + ".");
        TimeUnit.SECONDS.sleep(1);
    }
}
class DaemonSpawn implements Runnable {
    public void run() {
        while (true) {
            Thread.yield();
        }
    }
}
class Daemon implements Runnable {

    private Thread[] t = new Thread[10];

    public void run() {

        for (int i=0; i<t.length; i++) {
            t[i] = new Thread(new DaemonSpawn());
            t[i].start();
            System.out.println("DaemonSpawn " + i + " started.");
        }

        for (int i=0; i<t.length; i++) {
            System.out.println("t[" + i + "].isDaemon() = " +
                    t[i].isDaemon() + ".");
        }
        while (true) {
            Thread.yield();//让出CPU
        }
    }
}

(3) 设置关闭的标识符
通过一个标志位来标识线程是否被中断。但如果线程被阻塞那么,永远没有办法触发钩子函数来结束线程。
在Java提供了对这种情况的解决方法。
void interrupt()

中断线程,设置线程的中断位为true。如果线程此时恰好处于WAITING或者TIMED_WAITING状态,就会抛出一个InterruptedException,并且线程被唤醒

boolean isInterrupted()

检查线程的中断标记位,true-中断状态, false-非中断状态

static boolean interrupted()

静态方法,返回当前线程的中断标记位,同时清除中断标记,改为false。比如当前线程已中断,调用interrupted(),返回true, 同时将当前线程的中断标记位改为false, 再次调用interrupted(),会发现返回false。

2. synchronized关键字

(1)锁的对象是什么?
对于非静态成员函数,锁对象其实是加在对象上的;
对于静态成员函数,锁是在加类.class上的,当然class本身也是对象。
(2)锁的本质是什么?
锁是实现线程对资源的权限控制,从程序角度来看锁其实也是一个“对象”。这个“对象”需要完成3件事情。
1----这个对象内部需要一个标志位(state变量),记录自己有没有被某个线程占用。
2----如果这个对象被某个线程占用,得记录这个线程的thread ID,知道自己被哪个线程占用了。
3----这个对象还得维护一个thread ID list,记录其他所有阻塞的等待拿这个锁的线程。在当前线程释放锁之后,从这个thread id list里面取一个线程唤醒。
既然锁是一个“对象”,要访问的资源也是一个对象,那么两个对象可以合成一个对象。我们要访问一个对象也可以对这个对象加锁。synchronized(obj)。
(3)实现原理
Java对象头里面有一块数据叫Mark Word。里面有两个重要字段:锁标志位和占用该锁的thread ID。
synchronized还有偏向、自旋、等优化策略。
1.无锁
2.偏向锁,当一个线程多次获取同一把锁时,在Mark Word对象头中记录线程ID,重复获取时比较ID直接获取。
如果线程在持有偏向锁的过程中被阻塞或终止,那么偏向锁还需要进行额外的检查和恢复操作,这也会增加一定的开销。
3.轻量级锁,当不止一个线程竞争锁时,升级为轻量级锁,使用CAS自旋获取。
如果长时间获取不到锁升级为重量级锁。
4.重量级锁
(4)wait()和notify()
wait()是阻塞等待,notify()是随机唤醒一个阻塞的线程,都是Object类中的方法,为什么要放在基础类里面?为什么必须和synchronized一起使用?
两个线程之间要通信,对于同一个对象来说,一个线程调用该对象的wait(),另一个线程调用该对象的notify(),这个对象本身就得同步,首先就得获得锁。
synchronized关键字可以加在任何对象的成员函数上面,任何对象都可能称为锁。所以放在Object中。

3. volatile关键字

(1) 存在的问题?
1. 64位写入的原子性。
在32位机上64位数据得分两次写入,这样读取的线程可能会得到一半的值
2. 内存可见性
CPU中有三级缓存,每个核都有L1、L2缓存,L3缓存时共享的。存在缓存一致性问题,通过MESI CPU缓存一致性协议解决,但是对性能有很大的损耗,所以又做了一些优化,在计算单元和L1之间加了内存缓存、加载缓存等缓存。对应到Java中就是JMM内存模型,每个线程都有自己的一块本地内存,和共享内存之间存在内存可见性问题。
3. 重排序
例如单例模式,instance = new Instance()和存在问题。
(1)分配一块内存
(2)在内村上初始化成员变量
(3)把instance引用指向内存
(2)(3)可能重排序。
共存在三种重排序问题
(1)编译器重排序
(2)CPU指令重排序
(3)CPU内存重排序

x=1;a=y;和y=1;b=x;
因为内存重排序可能出现a=b=0;
(2)解决方法
定义happen-before规范,用于平衡开发者写程序的方便性和系统运行的效率。
1. 单线程中的每个操作 happend-before任意后续操作
2. 对volatile变量的写入,happen-before对应后续对这个变量的读取。
3. 对synchronized的解锁,happen-before对应后续对这个锁的加锁。
4. happen-before的传递性
5. final变量的写可见于后续的final变量的读。
(3)原理
在编译器和CPU层面都有对应的指令,也就是内存屏障。
编译器的内存屏障只是为了告诉编译器不要对执行进行重排序。当编译完成之后这种内存屏障就消失了。
CPU的内存屏障是CPU提供的指令,可以由开发者显示调用。
CPU内存屏障确保了指令不会被重排以及内存缓存及时刷新,其他CPU可见。
一种参考做法:
volatile在写操作前面插入写写屏障,后面插入写读屏障。
volatile在读操作后面插入读读屏障和读写屏障。

  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值