很详细的Java多线程知识

Java多线程


概述

进程拥有自己的一整套变量,而线程部署于进程中,同一个进程里的线程共享数据。
线程与进程先比带来的好处
1.效率:共享数据使得线程之间的通信比进程更加地有效和快捷。
2.代价:创建和销毁一个线程比创建和销毁一个进程的开销小得多。
线程存在的问题
线程共享一个进程里的数据,那么在线程不同的、访问进程共享资源的顺序下,可能会对数据造成同步问题。

线程的生命周期(六种状态)
New(新创建)
Runnable(可运行)
Blocked(阻塞)
Waiting(等待)
Timed waiting(限时等待)
Terminated(终止)
可以使用线程对象的getState方法获得线程的状态

促使终止有2种方式:
线程执行完了任务,自然终止;发生了一个异常(未被捕获)。

创建线程

Runnable接口方法:
1.创建一个继承自Runnable并重写了run方法的对象,将需要开启线程执行的任务代码放置到run方法中;
2.使用Thread类参数为Runnable对象的构造函数构建一个Thread对象;
3.使用Thread对象的start方法启动线程。
比如:

class TestRunnable implements Runable{
    @Override
    public void run(){
        //task code
    }
}

Runable test = new TestRunnable();
Thread thread = new Thread(test);
thread.start();

更多的时候采用匿名内部类的方式创建Thread对象:

new Thread(new Runnable{
    @Override
    public void run(){
        //task code
    }
}).start();

Thread类方法:
1.创建一个继承自Thread的类,重写run方法,将需要开启线程执行的代码放置到run方法中;
2.使用对象的start方法开启一个线程。
这种方法已经不推荐了,因为java是单继承的

需要注意的是,使用start方法后,线程不是处于运行状态,而是处于可运行状态,因为开启一个线程,如果没有被分到时间片,那么就不是处于运行状态,而是它本身是可运行的,只是处在了阻塞状态。

中断线程

每一个线程都有一个boolean的标志位,这个标志位用来标志线程是否被其他线程请求中断,然后线程有权决定如何响应其他线程的中断请求(大多数情况下进行终止本线程,因为中断请求本身就是我们自己写的,没理由自己不响应自己吧)。
请求中断的方式:调用需要其中断的线程的方法interrupt,这个方法会是该线程的标志位置true。
需要区别的是,不是使用interrupt(需要中断的线程对象)来操作的,而是使用需要中断的线程对象的这个方法,这也体现了控制权是在线程本身的,保证了线程的安全。

线程的属性

优先级

java线程的优先级默认有10个(其实依赖于宿主机平台的线程实现机制),可以通过线程对象的setPriority方法设定线程的优先级。
默认的常量:
MIN_PRIORITY:1
MAX_PRIORITY:10
NORM_PRIORITY:5
每当线程调度器有机会选择新的线程时,它会首先选择具有较高优先级的线程。

守护进程

守护进程也可以称为后台进程,是一个为其他线程做服务的线程,它是进程中最后一个被销毁的线程,当程序里只剩下守护线程时,虚拟机就退出了。
在调用线程对象的start方法前,通过使用线程对象的setDaemon(true),就可将该线程对象设置为守护线程了。

线程组、处理未捕获异常的处理器

异常有2种类型:已检查异常和未检查异常。已检查类型是指系统能识别的、编译能检测出来的、继承自IOException的异常;未检测异常指的是系统不能识别的、即编译时不能识别、运行时才发现的、继承自RuntimeException的异常。

在线程的run方法中,是不能抛出任何已检测异常的,也就是说,不能在run方法后面申明throws XXX异常,对于这类异常只能使用try和catch来捕获处理,不能传递出去。
而对于run里发生的未检测异常,java不使用catch来处理,而是用设置好的异常处理器来处理,这样,当未检测异常发生时,就会把异常传递给异常处理器去处理。

处理器接口:Thread.UncaughtExceptionHandler
需要重写的方法:void uncaughtException(Thread t, Throwable e)
为线程对象设置处理器:调用线程对象的setUncaughtExceptionHandler(handler)方法。
为所有线程对象设置默认的处理器:调用Thread的静态方法setDefaultUncaughtExceptionHandler(handler)

对于Thread的静态方法,如果没有调用,就为空,而对于线程对象各自的设置方法,如果没有调用,就以该线程的ThreadGroup作为默认的处理器。
ThreadGroup:线程组,是一个可以统一管理线程的集合,不过不建议使用,因为已经有更好的工具了。从ThreadGroup可以作为处理器知道,ThreadGroup也是继承了Thread.UncaughtExceptionHandler接口的。
既然ThreadGroup继承自处理器接口,那么它如何重写uncaughtException方法的呢?
主要实现以下逻辑:

if(ThreaGroup有父线程组)
then super.uncaughtException
else if (Thread的静态方法被设置了)
        then 使用Thread.getDefaultExceptionHandler
    else if (Throwable extends ThreadDeath) //ThreadDeath继承自Error
        then 什么也不做
        else 输出线程的名字+Throwable的栈踪迹 

同步

临界区:里面放置了无法同时被多个线程同时访问的、共享资源的程序片段。

当2个线程同时用非原子性的操作(即不是一步完成的操作)修改一个对象的数据时,称这种场景为竞争条件,英文为race condition,这种情形是会导致数据出错的。

ReentrantLock
使用ReentrantLock的方法:
Lock lock = new ReentrantLock();
lock.lock();
try{
    //临界区
}
finally{
    lock.unlock();
}

可重入锁:简单说就是嵌套锁,也就是一个对象里的锁里的临界区,里面又嵌套了子锁和子临界区,不过前提是这些嵌套锁是来自同一个锁对象。锁对象会保持一个持有计数来跟踪lock方法的嵌套调用。

条件对象

存在这样的一种情况:当某线程已经进入临界区了,但是发现它需要别的线程帮助它满足一定的条件之后才能继续执行它的任务,而此时,由于它已经锁定了临界区,在别的线程可能需要这个临界区才能帮助它完成条件时,就会让程序处于死亡状态。那么对于这种情况,java使用条件对象来管理这种情况。
条件对象是属于锁对象的,所以它是使用锁对象来创建的,寓意为,当满足某些条件时,锁才会锁上,使用方法:

//在临界区的线程
Condition testCondition = lock.newCondition();
testCondition.await();  //当发现某个条件不满足时
//帮助实现条件的线程
testCondition.signalAll(); //满足条件后

这个过程中,线程调用await方法后,会进入等待集里,进入阻塞状态,同时释放锁;当别的进程完成条件时,可以通过调singalAll方法通知所有的等待这个条件的线程,这些等待线程通通会被移除等待集,并且重新激活为可运行的状态;线程通过竞争后,在再次获得相应的锁时,就会从await调用返回,从阻塞的地方继续执行之前的任务。
在重新获得锁之后,线程最好再进行一次条件判断,或者直接使用循环进行判断

synchronized

虽然ReentrantLock和Condition接口为程序设计人员提供了可靠的锁控制,但是在很多时候,我们不需要那样去控制同步,synchronized可以更加地便捷。
使用synchronized修饰方法

public synchronized void function(){
    //需要同步的代码
}

这和下面的写法是等价的

public void function(){
    this.intrinsiclock.lock(); //获得对象的内部锁
    try{
        //需要同步的代码
    }
    finally{
        this.intrinsiclock.unlock();
    }
}

与Condition对应的,使用synchronized的方法内部会有一个内部条件对象,在使用时不需要明确地指出来。这个对象的内部条件的方法与Condition的几个方法分别对应:wait对应await、notifyAll对应signalAll。
如果修饰的时静态方法,那么对应的对象为类的类型信息,也就是Class对象。

synchronized存在一些缺陷:
1.没有中断方法,也就是没有对应ReentrantLock里的interrupt方法;
2.每个对象的内部条件只有一个,有些时候是不够用的;
3.在等待锁时,不能设置超时。

另外一种获得同步锁的方法,客户端锁定:
创建一个对象,然后使用该对象的内部锁:

Object obj = new Object();
synchronized(obj){
    //需要被同步的代码
}
监视器

监视器(monitor)就是用来监视线程同步的,它的意图是希望,可以在不需要程序员考虑如何加锁的情况下,保证多线程的安全性,也就是如何实现自动地加锁和解锁。
监视器的特点:域都是私有的,并且监视器的对象有一个相关的锁,该锁可以实现对所有方法进行同步操作,并且该锁可以有多个条件,以便于操作线程的通信交流。
监视器的构造图:

如果一个对象使用synchronized修饰方法,那么它就表现得像一个监视器,只是这种监视器存在缺陷:
1.域不是private;
2.方法要用synchronized修饰;
3.内部锁对其他对象是可用的,也就是客户端锁定。

Volatile(易变的)

如果需要被同步的实例域很小,使用锁来保护有时候会显得很没必要,使用锁是要开销的,因为要进行同步判断;另外,使用锁也会造成线程阻塞的。针对这种情况,java提供Volatile关键字,用它来修饰需要同步的实例域变量不需要加锁,它的作用是告诉虚拟机和编译器,被修饰的域有可能会发生同步问题。
不能因为需要被同步的实例域小而选择不加任何锁和volatile,原因如下:

1.计算机能够暂时在寄存器或者本地内存缓冲区中保持内存中的值,但是运行在不同处理器上的线程可能在同一个内存位置取到不同的值;
2.编译器可以改变指令的执行顺序,以便于达到最大吞吐量,不同线程可以修改内存的值。

而同步机制在本质上就是通过这些方面去完成保证线程安全的。比如保证数据域不放在寄存器或者保证数据域不会被重排序

Volatile是一种削弱的同步机制,或者说是一种比synchronized更轻量级的同步机制,它修饰的变量支持可见性和有序性,不支持原子性,比如说:

volatile int a;
a = 8; //支持的
a--; //不支持的,a--分为减法操作和赋值操作

原子性:在一个操作中,CPU不可以中途暂停然后再调度,既不会被中断操作,要么执行完成,要么完全不执行。
可见性:指线程之间,一个线程修改的状态对另一个线程是可见的,也就是线程A修改资源后,线程B能马上知道。
有序性:简单说就是线程间修改资源的执行是串行的。

使用volatile的条件
1.对变量的写操作不依赖于当前值,比如++操作;
2.该变量没有包含在具有其他变量的不变式中。
简而言之,volatile变量的有效值应该独立于任何程序的状态,包括变量的当前状态。

ThreadLocal

有些时候,既然变量共享会带来很多的风险或者同步开销,那么我们可以使用另外一种方式解决这个问题——不共享,为每个线程提供各自的共享变量的实例。
可以借助ThreadLocal辅助类来实现,它是线程的一个局部变量。为每个线程构造一个实例,可以使用以下的代码模型:

public static final ThreadLocal<局部变量类型> threadLocal = new ThreadLocal<>(){
    @Override
    protected 局部变量的类型 initialize(){
        return new 局部变量的构造方法;  //这里返回局部变量的实例
    }
}

局部变量类型 局部变量 = threadLocal.get();  //get方法调用threadLocal实例的initValue方法
锁测试

如果一个线程在请求锁时,有另一个线程正在使用该锁,那么该线程就会被阻塞,这保证了安全,却也存在一定的不合理,比如说这个线程如果请求不到锁,它可以先去做一些不需要这个锁的操作,而不是处于阻塞状态。java提供了tryLock方法进行锁测试,当使用锁对象的tryLock方法代替lock方法时,如果获得不到锁,线程不会被阻塞,而是马上返回false。
比如:

Lock lock = new ReentrantLock();
if (lock.tryLock()){
    try{
        //do someting;
    }
    finally{
        lock.unlock();
    }
}else{
    //做一些不需要锁的操作
}

也可以为锁测试设置它的测试限时,比如lock.tryLock(1000,TimeUnit.SECONDS)

读/写锁

对应的类为ReentrantReadWriteLock,它提供了读写控制的机制。
使用方法:

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();  //创建读写锁对象
Lock readLock = lock.readLock();  //从同一个对象里分别获取具体的锁,说明它们是一套的
Lock writeLock = lock.writeLock();

readLock.lock();
try{...} finally{ readLock.unlock(); }
writeLock.lock();
try{...} finally{ writeLock.unlock(); }

读写锁的控制机制是:
当某个线程对某个资源进行读操作时,会排斥其他线程关于这个对象的所有写操作,但是不排斥读操作;
当某个线程对某个资源进行写操作时,会排斥所有其他线程的读和写操作。

阻塞队列

许多线程问题可以通过使用一个或者多个队列来解决。生产者线程负责管理队列,消费者线程负责取出元素。
java中常见的阻塞队列:
LinkedBlockingDeque、ArrayBlockingQueue、PriorityBlockingQueue

线程安全的集合

让集合线程安全的方法1:使用系统自带的线程安全集合。
比如ConcurrentLinkedQueue、ConcurrentHashMap、ConcurrentSkipListMap、CopyOnWriteArrayList、CopyOnWriteArraySet等。
其中,有关Map提供了几个原子性的操作,比如putIfAbsent(key,value) 用于添加关联,remove(key,value)用于删除关联,replace(key,value)用户替换关联。

让集合线程安全的方法2:同步包装器。
同步包装器的通用模板:
指定的类型集合 对象 = Collections.synchronized+对应的类型集合(new 指定的具体类型集合);
比如:

List<String> list = Collections.synchronizedList(new ArrayList<String>());

Callable

Callable是在Runnable的基础上,多了可以返回参数的功能,它是一个参数化或者说泛型化的接口,它指定的类型就是就是最终返回的类型。
与Runnable使用run不同,Callable使用call作为需要重写的方法,同时call返回参数是在参数化时指定的类型。


public interface Callable<T>{
    T call throws Exception;
}

Future

Future是用来保存异步计算的结果的,可以将它附给一个线程,任务完成以后便可以获得相关信息。
更多到时候用于线程池的控制。
自带的方法:

T get();
boolean cancel(boolean mayInterrupt);
boolean isCanceled();
boolean isDone();

执行器

执行器的3大好处:
1.减少因为频繁创建线程和销毁线程的开销。
2.减少进程中线程的并发数量。
3.管理线程。
更具体的参考另外一篇文章:
Android中的多线程

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值