Java教程

java多线程基础

本文主要是介绍java多线程基础,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

进程、线程概念

进程是程序的运行实例。
进程是程序向操作系统申请资源的基本单位,线程是进程中可独立执行的最小单位。
一个进程至少包含一个线程,可以包含多个线程。
线程是进程的一条执行路径。
Linux中的JVM是基于pthread实现的,现在的Java中线程的本质,其实就是封装的操作系统中的线程。通过jstack也可以看到,每个java线程对应了一个操作系统线程。

线程基本使用

线程创建

方法一:定义Thread子类,重写run方法。调用start方法启动线程。
方法二:定义Runnable实例类,实现run方法,将Runnable实例传入Thread构造器创建线程。调用start方法启动线程。

线程特性

线程属于一次性用品,start方法只能调用一次,线程运行结束后所占资源会被JVM回收。
如果在当前线程直接调用线程实例的run方法,run方法里的代码仍然是在当前线程运行,只有执行start才会创建新线程运行run方法里的代码。

Thread类常用方法

static Thread currentThread();
返回当前代码的执行线程对象
void start();
启动相应线程
void join();
A线程中调用线程B的join方法,A线程会等B线程执行完然后再继续执MultiThread3类
static void sleep(long millis)
当前线程暂停指定时间
static void yield()
当前线程主动放弃对处理器的占用,但是此方法可能不生效,不可靠

线程同步

线程竞态

什么是竞态?
多线程之间线程交错,导致结果有时正确有时错误的现象叫做竞态。
竞态产生的模式
(1) 读改写;
(2) 检查而后行动

如何避免竞态,保障线程安全

保障原子性、可见性、有序性

  • 原子性
    一个线程操作共享变量的一组操作在其他线程看来要么都没发生,要么同时都发生,其他线程不会看到这些操作只执行一半的情况;一个线程操作共享变量的一组操作不能和其他线程操作该共享变量的一组操作同时进行。
  • 可见性
    一个线程对共享变量的修改后,另一个线程可以马上看到共享变量修改后的相对新值(不可见可能是JIT编译优化导致,可能是缓存同步不及时导致)
  • 有序性
    单线程的代码执行具有貌似串行语义,即代码看上去是顺序执行的,但是在多线程这种貌似串行语义无法保障(指令重排序、存储系统重排序)。需要保障多线程下程序运行的有序性。

线程同步机制

同步机制包括锁(内部锁、显式锁)、volatile等

锁可以理解为共享数据的许可证,一个线程在获取了锁后才能对共享变量进行访问操作,一般的锁一次只能被一个线程持有,这样将多个线程对共享变量的并发访问变成了串行访问,避免了竞态的产生。
锁可以保证指定代码集合执行的原子性、可见性、有序性。即同步在同一个锁上的不同代码集合不会并行执行,进入同步代码之前的代码的结果对同步代码可见,同步代码的结果对后续代码可见,同步代码内部的代码不会被重排序到同步代码外部。
锁分为内部锁和外部锁:

  • 内部锁
    内部锁通过synchronized关键字实现,synchronized可以用于代码块,也可以用于方法
    同步静态方法,锁为类对象
    如果同步实例方法,锁为this,即当前对象实例
    synchronized关键字申请锁和释放锁都是JVM代为实施,不需要手动操作,所以称为内部锁,内部锁是使用最方便的锁。
  • 显示锁
    显示锁最常用的是ReentrantLock,需要创建一个ReentrantLock对象lock,并且在同步代码前插入lock.lock(),在同步代码后插入lock.unlock();,注意lock.unlock();需要放在finally里中,避免锁泄露;
  • 锁的调度
    公平调度,多个线程申请锁的时候需要排队,先到先得,不允许插队,在线程任务执行时间较长的情况下使用公平调度效率更高,公平调度适合线程任务执行时间较长的场景;
    非公平调度,多个线程申请锁的时候需要排队,但是允许插队,一般情况下效率更高;
    内部锁只支持非公平调度,ReentrantLock同时支持公平和非公平调度
  • 内部锁和ReentrantLock对比
    两者性能接近,内部锁使用简单,ReentrantLock使用复杂,需要手动释放锁。一般情况下使用synchronized内部锁即可,如果需要使用更加复杂的线程协作或者需要公平调度等时候可以考虑使用ReentrantLock。

volatile

volatile的作用:保证可见性、有序性、long/double变量的读写原子性
有序性:
禁止Volatile写操作与该操作之前的任何读写操作进行重排序,保证Volatile写操作之前的任何读写操作都会先于volatile写操作被提交,即其他线程看到写线程对volatile变量的更新时,写线程在更新volatile变量之前所执行的内存操作结果对于读线程均是可见的。保障了读线程对写线程在更新volatile变量前对共享变量所执行的更新操作的感知顺序与相应的源代码一致,即保障了有序性。
禁止volatile读操作之后的任何读写操作和volatile读操作进行重排序,保障了volatile读操作之后的任何操作开始执行之前,写操作对相关共享变量的更新已经对当前线程可见。
举例:
以下操作中,操作1和操作2不能被重排序到Volatile写操作之后,操作5和操作6不能被重排序到Volatile读操作之前,由此保证了有序性。

操作1
操作2
Volatile写操作
操作3
操作4
Volatile读操作
操作5
操作6

可见性:
当一个线程对volatile变量修改后,另一个线程可以及时看到volatile的修改后的新值;
long/double变量的读写原子性:
JVM不保障long/double变量的写原子性,在多线程环境下可以用volatile关键字保障。

场景:
volatile最常用于保障可见性和修饰状态标志

原子变量

例如AtomicInteger、AtomicLong等

public static AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();

执行incrementAndGet()方法可以实现线程安全的自增,不用在外部加锁

线程安全的集合

常用的集合并不是线程安全的,我们可以借助一些特定的集合保证线程安全性。
例如采用ConcurrentHashMap代替HashMap,利用CopyOnWriteArrayList或者Collections.synchronizedList(new ArrayList())代替ArrayList,前者适合读场景,后者适合其他场景。其他线程安全集合可以自行查阅资料。

这篇关于java多线程基础的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!