Java多线程学习

线程的调度

  1. 抢占式(重点)
    在抢占模式下,操作系统负责分配CPU时间给各个进程,一旦当前的进程使用完分配给自己的CPU时间,操作系统将决定下一个占用CPU时间的是哪一个线程。
  2. 协作式
    协作式线程调度器在将cpu控制权交给其他线程钱,会等待正在运行的线程自己去暂停,然后才可以交给另外一个线程。一些早期或者特殊用途的虚拟机 可能会使用这种方式。

进程与线程

进程的运行需要较多的资源,因此,操作系统能够同时运行的进程数量是有限的。进程间的切换与通信也存在较大的开销。为了能够并行的执行更多的任务,提升系统的效率,才引入了线程,线程间的切换开销比进程间的切换开销小得多。因此线程是运行的基本单元(CPU调度的基本单位)、进程是资源分配的基本单元
线程是进程的一部分,一个进程拥有1~~N个线程,线程共享所在进程的资源。
线程分为守护线程和用户线程。

  • 守护线程:做一些辅助操作,程序不会等待守护线程结束而结束。
    thread.setDaemon(true)将一个线程设置为守护线程必须在thread.start()之前设置。
    在Daemon线程中产生的新线程也是Daemon的。
    不是所有的应用都可以分配给Daemon线程来进行服务的,比如读写操作或者计算逻辑。
  • 用户现场:用户线程结束,程序也就结束了,即使守护线程没有终止也会随之停止。

线程的生命周期

在这里插入图片描述

  • 新建:线程被new出来的状态。
  • 就绪:使用start开启线程,此时线程不一定会执行,处于竞争资源的时候。
  • 运行:获取到了运行资源,执行代码。
  • 阻塞:不执行代码,暂时放弃CPU的使用权,有可能会释放锁,进行等待,直到满足某个条件时回到就绪状态。
  • 死亡:线程当中代码执行完毕或者执行过程中出现异常,或被强行中断(不建议)。

线程的创建

继承Thread类

通过继承Thread类,覆盖其run()方法即可编写一个线程。

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }
}

new MyThread().start();让线程处于就绪状态。

实现Runable接口

Java单继承的特性并不满足开发需求所以可以通过实现Runable接口来创建线程。

public class MyThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }
}

new Thread(new Mythread()).start();让线程处于就绪状态。

Thread与Runable联系

在Runable接口源码中其实只定义了一个抽象的run方法。
Runable源码。

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

Thread实现了Runable并对其功能进行了扩展,扩展的功能主要还是通过JNI调取的由C++/C写的代码。
Thread类部分源码。

public
class Thread implements Runnable {
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
    ······
    ······
    ······
    ······
    ······
    ······
}

Callable与Future

Java5添加了Callable接口和Future接口,使得线程可以产生返回值,并且可以抛出异常。

public class MyThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        sleep(3000);
        return 10;
    }
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        Future<Integer>f=executor.submit(new MyThread());
        try {
            System.out.println(f.get()); //执行到此处会被阻塞,直到获取到返回值。
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        executor.shutdown();
    }
}

常用方法介绍

方法作用
t.start();启动线程t
t.checkAccess();检查当前线程是否有权限访问线程t
t.interrupt();尝试通知线程t中断
Thread.currentThread().isInterrupted()检查当前线程是否被要求中断
t.setPriority(int)设置线程的优先级,1~10依次增大
t.isDaemon();判断线程t是否为守护线程
t.setDaemon(boolean)在start前设置线程t是否为守护线程,默认false
t.isAlive();判断线程t是否存活
t.join(long);等待线程t终止在执行 ,参数为超时时间
t.setName(String);设置线程名称
yield();让当前线程让出CPU,转为就绪状态,重新争夺资源,当只有大于等于t优先级的线程才能被执行。
Thread.currentThread();得到当前线程对象的引用
Thread.sleep(long)让当前线程转为阻塞状态,参数表示阻塞时间,后转为就绪状态,不会释放锁
wait(long);Object当中的方法,和sleep类似,但是会释放锁,所以只能在synchronized作用域中使用
t.notify();wait()的对应方法,可以唤醒线程t,同样在synchronized作用域中使用

线程安全与效率

Java多线程的内存模型

在这里插入图片描述
我们声明的对象的成员变量、静态变量等非局部变量都位于主内存中,而每个线程又都有自己的工作内存,工作内存是CPU中寄存器与高速缓存的抽象描述,并不是真正的一块内存,一般情况下,当主线程获得CPU使用权后,开始执行前会将主内存当中的变量load到工作内存,处理完成在save会主内存。

可能产生的问题

线程安全主要考虑的三个方面

  • 可见性:一个线程对某个变量修改时,其他线程可以立即检测到。
  • 有序性:禁止指令重排。
  • 原子性:线程在执行某一连续操作时要么都执行,要么都不执行。

线程同步的机制(四个)

  • 临界区:在同一时刻只允许一个线程执行的代码块称为临界区,临界区通常用锁机制来实现。
  • 互斥量:互斥量跟临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。
  • 信号量:信号量允许有限数量的线程在同一时刻访问同一资源。
  • 事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。

可能产生的问题

  • 饥饿:某个线程长时间得不到所需要的资源,而总是经常性的处于就绪状态。
  • 死锁:两个或两个以上线程在资源竞争中互不相让,形成资源等待或长时间处于阻塞状态。

如何确保线程安全

volatile关键字

作用:禁止指令重排和保证其可见性,但是不能解决原子性。

原理: 被volatile关键字修饰的变量会存在一个“lock:”的前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能,它的作用是将本处理器的缓存写入了内存,该写入动作也会引起别的处理器或者别的内核无效化(Invalidate,MESI协议的I状态)其缓存,这种操作相当于对工作内存与主内存中的变量做了一次“store和write”操作。所以通过这样一个操作,可让前面volatile变量的修改对其他处理器立即可见。lock指令的更底层实现:如果支持缓存行会加缓存锁(MESI);如果不支持缓存锁,会加总线锁。

CAS操作

更新某个变量前,检查变量的当前值是否符合期望值,如果相符就使用新值替换当前值,否则自旋尝试重新获取,直到成功。

可能存在的问题:

  • ABA问题:如果一个值原来是A,接着变成了B,之后又变成了A,执行CAS时会发现值没有预期当中的变化不执行。
  • 循环开销:当冲突严重时,自旋的开销严重增加CPU负担。
  • 只能保证一个共享变量的原子性。

synchronized关键字

是Java的关键字,被用于标记一个方法或代码块,通过给对象上锁的方式,将自己的作用域变为一个临界区,由编译器负责加锁与解锁的操作,是自动进行的。

synchronized悲观锁、偏向锁—>轻量级锁—>重量级锁(逐渐升级)、非公平锁、可重入锁、独占锁

作用范围:

  • 修饰代码块:
        synchronized(data){
            ······
            ······       
        }

作用范围是{}括起来的代码块,作用对象是()内的对象,表示获得该对象的锁。

  • 修饰非静态方法:
    public synchronized void set(){
        ······
        ······
    }

被修饰的方法称为同步方法,其作用范围是整个方法,作用的对象是拥有这个方法的对象,执行此方法时会先获取调用此方法的对象的锁。
注意:在定义接口方法时不能使用synchronized来修饰。构造方法也不能用synchronized来修饰,但是构造方法体内可以使用synchronized来修饰。

  • 修饰静态方法:
    public synchronized static void set(){
        ······
        ······
    }

其作用范围是整个静态方法,作用对象是该静态方法所在的类(静态方法属于类不属于对象),锁定的是该类本身。

  • 修饰一个类:
        synchronized(MyThread.class){
            ······
            ······
        }

作用范围是{}括起来的部分,和修饰静态方法一样,获取的是类本身的锁。

Lock接口

与synchronized类似,对需要的对象添加锁,与synchronized不同,需要unlock主动去解锁,最好放在finally块当中。

具体实现类:

实现类作用
ReentrantLock()可重入锁
ReentrantReadWriteLock()读写锁
readLock()读锁(在ReentrantReadWriteLock类当中获取)
writeLock()写锁(在ReentrantReadWriteLock类当中获取)

各种概念锁

解释
乐观锁总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据(CAS操作)。
悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁
自旋锁线程循环地去获取锁
非自旋锁/互斥锁普通锁。获取不到锁,线程就进入阻塞状态。
适应性自旋锁自旋一定次数或时间获取不到锁变为非自旋锁
偏向锁无实际竞争,且将来只有第一个申请锁的线程会使用锁。
轻量级锁无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。可以优化为自旋形式
重量级锁有实际竞争,且锁竞争时间长。一般为互斥形式
公平锁指在分配锁前检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程(先来先服务)
非公平锁指在分配锁时不考虑线程排队等待的情况,直接尝试获取锁,在获取不到锁时再排到队尾等待(谁抢到是谁的)
可重入锁/递归锁即若当前线程执行某个方法已经获取了该锁,那么在该方法或其调用的方法中尝试再次获取锁时,可以获取到
不可重入锁即若当前线程执行某个方法已经获取了该锁,那么在该方法或其调用的方法中尝试再次获取锁时,就会获取不到被阻塞。
排他锁/写锁/独占锁只能被一个线程读取和修改,其他线程获取时会被阻塞
共享锁/读锁允许多个线程读取,但不能修改
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值