java多线程学习

本文深入探讨Java多线程的创建方式,包括继承Thread类、实现Runnable接口和Callable接口,对比各自优劣。解析线程的生命周期,各种线程状态及其转换。详细解释synchronized关键字和ReentrantLock的使用,以及它们之间的区别。此外,还介绍了ThreadLocal的用途和实现机制,atomic类的高效原子操作,以及特殊线程如僵尸线程和孤儿线程的概念。
摘要由CSDN通过智能技术生成

定义

进程是资源分配的基本单元,进程间资源不共享。

线程是独立运行和独立调度的基本单元,一个进程中可以包含多个线程,这些线程共享进程内的资源。每个线程可以拥有自己的堆栈,自己的程序计数器和自己的局部变量。

创建多线程

继承Thread方法

(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。

(2)创建Thread子类的实例,即创建了线程对象。

(3)调用线程对象的start()方法来启动该线程。

public class ThreadTest {
    public static void main(String[] args){
        MyThread1[] thread1s = new MyThread1[10];
        for(int i=0;i<10;i++){
            thread1s[i] = new MyThread1();
            thread1s[i].start();
        }
    }
}
class MyThread1 extends Thread{
    @Override
    public void run(){
        /**线程运行内容**/
        System.out.println(Thread.currentThread().getName());
        /**sleep方法**/
        try {
            sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

实现Runnable接口

(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

(2)创建 Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

(3)调用线程对象的start()方法来启动该线程。

public class ThreadTest {
    public static void main(String[] args){
        Thread[] threads = new Thread[10];
        for(int i=0;i<10;i++){
            threads[i] = new Thread(new MyRunnable());
            threads[i].start();
        }
    }
}
class MyRunnable implements Runnable{
    @Override
    public void run() {
        /**线程运行内容**/
        System.out.println(Thread.currentThread().getName());
    }
}

 

实现Callable接口

(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。(FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。)

(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

public class ThreadTest {
    public static void main(String[] args){
        Thread[] threads = new Thread[10];
        for(int i=0;i<10;i++){
            MyCallable mc = new MyCallable();
            FutureTask<Double> ft = new FutureTask<Double>(mc);
            threads[i] = new Thread(ft);
            threads[i].start();
            
            //可以有异常的处理
            try
            {
                System.out.println("子线程的返回值:"+ft.get());
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            } catch (ExecutionException e)
            {
                e.printStackTrace();
            }
        }
    }
}
class MyCallable implements Callable{
    //可以有返回值
    @Override public Double call() throws Exception {
        /**线程运行内容**/
        System.out.println(Thread.currentThread().getName());
        return Math.random();
    }
}

 

三种创建方式的区别

1、采用实现Runnable、Callable接口的方式创建多线程

优势:线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况。

劣势:编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

 

2、使用继承Thread类的方式创建多线程

优势:编写简单,如果需要访问当前线程,直接使用this即可获得当前线程。

劣势是:线程类已经继承了Thread类,所以不能再继承其他父类。

 

3、Runnable和Callable的区别

(1) Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。

(2) Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。

(3) call方法可以抛出异常,run方法不可以。

(4) 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

生命周期

  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的成为“运行”。 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的时间片前处于就绪状态(ready)。获得cpu 时间片后变为运行中状态(running)。
  3. 阻塞(BLOCKED):表线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  5. 超时等待(TIME_WAITING):该状态不同于WAITING,它可以在指定的时间内自行返回。
  6. 终止(TERMINATED):表示该线程已经执行完毕。

 

 

 

 

几种方法的比较

  1. Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIME_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
  2. Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的cpu时间片,由运行状态变会就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。
  3. t.join()/t.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入TIME_WAITING/TIME_WAITING状态,当前线程不释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程进入就绪状态。
  4. obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout)timeout时间到自动唤醒。
  5. obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。

 

Condition

 

同步

synchronized

synchronized可以修饰方法也可以修饰代码块

实现原理:

本质上是对一个对象的监视器(monitor)的获取。任意一个对象都拥有自己的监视器,当同步代码块或同步方法时,执行方法的线程必须先获得该对象的监视器才能进入同步块或同步方法,没有获取到监视器的线程将会被阻塞,并进入同步队列,状态变为BLOCKED。当成功获取监视器的线程释放了锁后,会唤醒阻塞在同步队列的线程,使其重新尝试对监视器的获取。

 

  注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

public class Synchronization {
    private double value = 0;
    public synchronized void myFun1(){
        value = Math.random();
        System.out.println(value);
    }
    public void myFun2(){
        synchronized (Synchronization.class){
            value = Math.random();
            System.out.println(value);
        }
    }
}

 

可重入锁

当一个线程得到一个对象后,再次请求该对象锁时是可以再次得到该对象的锁的。 具体概念就是:自己可以再次获取自己的内部锁。 Java里面内置锁(synchronized)和Lock(ReentrantLock)都是可重入的。

public class SynchronizedTest {
    public void method1() {
        synchronized (SynchronizedTest.class) {
            System.out.println("方法1获得ReentrantTest的锁运行了");
            method2();
        }
    }
    public void method2() {
        synchronized (SynchronizedTest.class) {
            System.out.println("方法1里面调用的方法2重入锁,也正常运行了");
        }
    }
    public static void main(String[] args) {
        new SynchronizedTest().method1();
    }
//调用method1()方法时,已经获得了锁,此时内部调用method2()方法时,由于本身已经具有该锁,所以可以再次获取。
}

ReentrantLock

需要显示加锁和释放锁

void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁. boolean tryLock():如果锁可用, 则获取锁, 并立即返回true, 否则返回false. 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用, 当前线程仍然继续往下执行代码. 而lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行. 通常采用如下的代码形式调用tryLock()方法: void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生. Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将缩放锁。

private ReentrantLock myLock = new ReentrantLock();
public void myFun3(){
    //手动加锁
    myLock.lock();
    try{
        value = Math.random();
        System.out.println(value);
    }finally {
        //手动释放锁
        myLock.unlock();
    }
}

 

synchronized和ReentrantLock的区别

  1. 可中断锁

顾名思义,就是可以相应中断的锁。

在Java中,synchronized就不是可中断锁,而Lock是可中断锁。如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

lockInterruptibly()的用法体现了Lock的可中断性。

 

  1. 公平锁

公平锁是指多个线程在等待同一个锁时,必须按照申请的时间顺序来依次获得锁;而非公平锁则不能保证这一点。非公平锁在锁被释放时,任何一个等待锁的线程都有机会获得锁。  synchronized的锁是非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过带布尔值的构造函数要求使用公平锁。

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
}
//创建公平锁
private static ReentrantLock lock=new ReentrantLock(true);

 

  1. 死锁

synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

 

  1. 读写锁

读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。

正因为有了读写锁,才使得多个线程之间的读操作可以并发进行,不需要同步,而写操作需要同步进行,提高了效率。

ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。

可以通过readLock()获取读锁,通过writeLock()获取写锁。

 

  1. 绑定多个条件

一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多余一个条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这么做,只需要多次调用new Condition()方法即可。

volatile

java当中的实例对象、数组元素都放在java堆中,java堆是线程共享的。我们把共享的内存称为主内存,每个线程私有的内存称为工作内存,当线程1需要与线程2通信时,需要经过以下几个步骤【涉及到JMM内存模型】:

  1. 线程1工作内存将X=1执行store操作,对主内存实行write操作将X=1写入主内存。
  2. 线程2实行read操作读取主内存的X=1,工作内存实行load操作,将X=1加载入自己的工作内存。

存在的问题:由于工作内存这个中间层的出现,线程1和线程2必然存在延迟的问题,例如线程1在工作内存中更新了变量,但还没刷新到主内存,而此时线程2获取到的变量值就是未更新的变量值,又或者线程1成功将变量更新到主内存,但线程2依然使用自己工作内存中的变量值,同样会出问题。

 

 

volatile的作用:

多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

  1. 一个变量被volatile修饰后,则表示本地内存无效,每次读取时需要从主内存中读取。
  2. 当一个volatile变量被修改后,则立即写入主内存。

 

实现:

加入volatile关键字时,会多出一个lock前缀指令。lock前缀指令其实就相当于一个内存屏障。内存屏障是一组处理指令,用来实现对内存操作的顺序限制,避免JMM重排序。volatile的底层就是通过内存屏障来实现的。

 

使用场景:状态修改;单例模式双检锁。

 

ThreadLocal

ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

public T get() { } //获取ThreadLocal在当前线程中保存的变量副本,get之前必须得set,否则会报异常
public void set(T value) { }  //设置当前线程中变量的副本
public void remove() { } //用来移除当前线程中变量的副本
protected T initialValue() { } //一般是用来在使用时进行重写

使用实例:

public class MyThreadLocal {
    /**必须是public static 类型**/
    public static ThreadLocal<Integer> tl1 = new ThreadLocal<Integer>();
    public static ThreadLocal<String> tl2 = new ThreadLocal<String>();
}
public class ThreadLocalTest {
    public static void main(String[] args){
        MyThreadLocal.tl1.set(1);
        MyThreadLocal.tl2.set("test1");
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        thread1.start();
        thread2.start();
    }
}
class MyThread extends Thread{
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            MyThreadLocal.tl1.set(i);
            MyThreadLocal.tl2.set("test"+i);
            try {
                sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.getName()+" int:"+MyThreadLocal.tl1.get());
            System.out.println(this.getName()+" Str:"+MyThreadLocal.tl2.get());
        }
    }
}
Thread-0 int:0
Thread-1 int:0
Thread-0 Str:test0
Thread-1 Str:test0
Thread-1 int:1
Thread-0 int:1
Thread-1 Str:test1
Thread-0 Str:test1
Thread-1 int:2
Thread-1 Str:test2
Thread-0 int:2
Thread-0 Str:test2
Thread-0 int:3
Thread-1 int:3
Thread-1 Str:test3
Thread-0 Str:test3
Thread-0 int:4
Thread-0 Str:test4
Thread-1 int:4
Thread-1 Str:test4
Thread-1 int:5
Thread-1 Str:test5
Thread-0 int:5
Thread-0 Str:test5
Thread-1 int:6
Thread-0 int:6
Thread-0 Str:test6
Thread-1 Str:test6
Thread-0 int:7
Thread-0 Str:test7
Thread-1 int:7
Thread-1 Str:test7
Thread-0 int:8
Thread-0 Str:test8
Thread-1 int:8
Thread-1 Str:test8
Thread-0 int:9
Thread-0 Str:test9
Thread-1 int:9
Thread-1 Str:test9

实现源码解读:

每个线程有个threadLocals的属性,这个属性是ThreadLocalMap类型,这个map的key:threadLocal对象,value:储存的值的对象。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
}

 

 

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

concurrent

atomic [java.util.concurrent.atomic]

  • 原子类其内部实现不是简单的使用synchronized,而是一个更为高效的方式CAS (compare and swap) + volatile和native方法(同步的工作更多的交给了硬件),从而避免了synchronized的高开销,执行效率大为提升
  • 虽然基于CAS的线程安全机制很好很高效,但要说的是,并非所有线程安全都可以用这样的方法来实现,这只适合一些粒度比较小,型如计数器这样的需求用起来才有效,否则也不会有锁的存在了
  • 在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新字段。Atomic包里的类基本都是使用Unsafe实现的包装类。 

标量类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

更新器类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

复合变量类:AtomicMarkableReference,AtomicStampedReference

 

Modifier and Type

Method and Description

int

accumulateAndGet(int x,IntBinaryOperator accumulatorFunction)

自动更新当前值与给定的功能应用到当前和给定值的结果,返回更新后的值。

int

addAndGet(int delta)

自动添加给定值和当前值。

boolean

compareAndSet(int expect, int update)

自动设置的值来指定更新值,如果电流值 ==期望值。

int

decrementAndGet()

原子由一个电流值递减。

double

doubleValue()

为扩大基本转换后的 double返回该 AtomicInteger价值。

float

floatValue()

为扩大基本转换后的 float返回该 AtomicInteger价值。

int

get()

获取当前值。

int

getAndAccumulate(int x,IntBinaryOperator accumulatorFunction)

自动更新当前值与给定的功能应用到当前和给定值的结果,返回前一个值。

int

getAndAdd(int delta)

自动添加给定值和当前值。

int

getAndDecrement()

原子由一个电流值递减。

int

getAndIncrement()

原子逐个增加电流值。

int

getAndSet(int newValue)

自动设置为给定的值并返回旧值。

int

getAndUpdate(IntUnaryOperator updateFunction)

自动更新当前值与结果应用给定的函数,返回前一个值。

int

incrementAndGet()

原子逐个增加电流值。

int

intValue()

作为一个 int返回该 AtomicInteger价值。

void

lazySet(int newValue)

最终设置为给定的值。

long

longValue()

为扩大基本转换后的 long返回该 AtomicInteger价值。

void

set(int newValue)

给定值的集合。

String

toString()

返回当前值的字符串表示形式。

int

updateAndGet(IntUnaryOperator updateFunction)

自动更新当前值与结果应用给定的函数,返回更新后的值。

boolean

weakCompareAndSet(int expect, int update)

自动设置的值来指定更新值,如果电流值 ==期望值。

 

源码解读:

public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
/******
var2: 修改前的值的地址[期待值]
var4: 在原值上加的数值
*******/
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            //获得现在内存内的值
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
            //var2期待的原内存值,var5现在的内存值,var5+var4修改后的值
            //只有当期待值和内存值相等时,才进行修改
        return var5;
    }

 

 

特性

原子性

相当于事务的概念,即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

在java中多用锁来保证原子性,例如synchronized,ReentrantLock

int i=0; //1
int j=i; //2
i++;     //3
j=i+1;   //4
/********
以上四个操作其实只有操作1是原子性的。
1—在Java中,对基本数据类型的变量和赋值操作都是原子性操作;
  [注:在32位的JDK环境下,对64位数据的读取不是原子性操作*,如long、double]
2—包含了两个操作:读取i,将i值赋值给j
3—包含了三个操作:读取i值、i + 1 、将+1结果赋值给i;
4—同三一样
*************/

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

Java提供了volatile来保证可见性。

有序性

有序性:即程序执行的顺序按照代码的先后顺序执行。

在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序它不会影响单线程的运行结果,但是对多线程会有影响。

Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。

public class DoubleCheckSinglenton {
    //双检索单例模式:【双检】在进入get方法时判断对象是否为空,当创建对象时再次判断对象是否为空
    //【锁】对创建对象的代码块进行加锁
    private volatile static DoubleCheckSinglenton instance = null;
    private DoubleCheckSinglenton(){};
    public static DoubleCheckSinglenton getInstance() throws InterruptedException {
        if(instance==null){
            synchronized (DoubleCheckSinglenton.class){
                Thread.sleep(300);
                if(instance==null){
                    instance = new DoubleCheckSinglenton();
                }
            }
        }
        return instance;
    }
}

特殊线程

僵尸线程

僵尸进程当前进程运行结束后,其父进程仍在运行或仍未结束并且父进程没有调用wait来清理以结束的子进程,或者一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

 

如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

孤儿线程

孤儿进程当前进程仍在运行时其父进程运行结束了,或者一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。

 

孤儿进程将被init进程(进程号为1)所收养,并由内核init进程对它们完成状态收集工作,而init进程会循环地wait()它的已经退出的子进程。因此孤儿进程并不会有什么危害。

 

孤儿进程和僵尸进程都可能使系统不能产生新的进程,都应该避免

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值