早期的计算机不包含操作系统,它们从头到尾执行一个程序,这个程序可以访问计算机中的所有资源。在这种情况下,每次都只能运行一个程序,对于昂贵的计算机资源来说是一种严重的浪费。
操作系统出现后,计算机可以运行多个程序,不同的程序在单独的进程中运行。操作系统负责为各个独立的进程分配各种资源。并且不同的进程间可以通过一些通信机制来交换数据,比如:套接字、信号处理器、共享内存、信号量等。
一、了解多线程
1.1 进程与线程
想必大家都听说过这两个名词,它们之间有什么联系与不同呢?
记得当时上操作系统课时,书上有这么一句话:进程是独立拥有 cpu 资源的最 小单位,线程是接受 cpu 调度的最小单位。网上看了那么多的解释,还是觉得书上说的最简单明了。
进程中可以同时存在多个程序控制流程的线程,线程会共享进程范围内的资源,因此一个进程可以有多个线程。打个比方,进程就像一个大工厂,而线程就是工厂的工人,多个工人负责协同完成工厂的任务。
1.2 多线程的优势
- 发挥多处理器的强大能力
- 简化建模过程
- 简化异步事件处理机制
- 响应更灵敏的用户界面
1.3 多线程安全性问题
多线程是一把双刃剑,使用多线程时意味着对开发人员有一定的技术要求。可见学会了多线程技术,就能写出更优雅的代码了,哈哈~
多个线程同时访问意味着“共享”变量,这些“共享”变量往往在其生命周期内是可变的,这样以来就极易出现多线程安全性问题。
什么是线程安全,这里给一个比较准确的定义:当多个线程访问某各类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
二、Java 内存模型
为什么要在多线程专题里涉及到 Java 内存模型呢?我觉得它可以帮助我们更好的理解多线程的工作机制。
比如很多人都听说过或了解过 volatile
关键字,都知道它能保证内存可见性问题,但是理解起来总是太过抽象。如果你了解了 Java 内存模型,它可以很好的帮助你理解这些问题。
Java 内存模型规定了所有的变量都存储在主内存中(Main Memory)中(可以类比为物理硬件的主内存)。每条线程都有自己的工作内存(Working Memory),线程的工作内存中保存了主内存中的变量的拷贝,线程对变量的所有操作(读取、赋值)都必须在自己的工作内存中进行,而不能直接读写主内存中的变量。
不同的线程之间无法访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
如果你对 Java 内存区域了解的话,很容易就会想工作内存、主内存与 Java 内存区域中的堆、栈、方法区是否有一定关系呢?
如果在实例变量的角度来看,主内存对应于 Java 堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。从更低层次来看,主内存直接对应于物理硬件的内存。
三、解决线程安全性问题
使用 Java 创建线程的几种方式这里就不再赘述了,我们来了解几种处理多线程安全性问题的方法。
3.1 synchronized 关键字
Java 提供了一种内置的锁机制(synchronized
关键字)来保证原子性与内存可见性。关于原子性与内存可见性问题会在讲述 volatile
关键字时详细的介绍,感兴趣的可以关注后面的文章。
Java 的内置锁是一种互斥锁,意味着最多只有一个线程能持有这种锁。当线程 A 和线程 B 同时访问临界资源,如果线程 A 获取锁,线程 B 就必须等待或阻塞,A 不释放 B 就只能等待下去。
由于每次只有一个线程执行内置锁内的代码,因此被 synchronized
关键字保护的临界资源会以原子(一组不可分割的单元)的方式执行,多个线程间执行时不会受到干扰,原子性与数据库事务有相同的含义。
synchronized
关键字可以用来修饰方法和代码块,如果修饰非静态方法和同步代码块,使用的锁是当前对象,如果修饰静态方法和静态代码块,使用的是当前类的 Class 对象作为锁。
使用方式如下
public class SyncTest {
private Integer num = 1;
public int numAutoIncrement() {
synchronized (this) {
return num ++;
}
}
public static void main(String[] args) {
SyncTest syncTest = new SyncTest();
// 开启 10 个线程
for (int i = 0; i < 10; i++) {
new Thread(() ->
System.out.println(Thread.currentThread().getName()
+ ":" + syncTest.numAutoIncrement())
).start();
}
}
}
3.2 使用原子类
jdk1.5 之后 java.util.concurrent.atomic
包下引入了诸如 AtomicInteger
、AtomicLong
等特殊的原子性变量类。
这些变量类底层使用 CAS 算法与 volatile
关键字确保对所有的计数器操作都能保证原子性与可见性,也就解决了多线程安全问题。
使用方式如下
public class SyncTest {
private AtomicInteger atomicInteger = new AtomicInteger(1);
public int numAutoIncrement() {
return atomicInteger.getAndIncrement();
}
public static void main(String[] args) {
SyncTest syncTest = new SyncTest();
for (int i = 0; i < 10; i++) {
new Thread(() ->
System.out.println(Thread.currentThread().getName()
+ ":" + syncTest.numAutoIncrement())
).start();
}
}
}
3.3 使用显示 Lock 锁
jdk1.5 之前,解决多线程共享对象访问的机制只有 synchronized
和 volatile
。jdk1.5 新增了一种新的机制:ReentrantLock
。ReentrantLock
并不是为替代内置锁而生的,当内置锁机制不适用时,可以考虑使用这种更灵活的加锁机制。
使用方式如下
public class SyncTest {
private Lock lock = new ReentrantLock();
private Integer num = 1;
public int numAutoIncrement() {
lock.lock();
try {
return num++;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
SyncTest syncTest = new SyncTest();
for (int i = 0; i < 10; i++) {
new Thread(() ->
System.out.println(Thread.currentThread().getName()
+ ":" + syncTest.numAutoIncrement())
).start();
}
}
}
从上面的代码可以看出来,使用显示锁机制相对内置锁要麻烦一些,使用时要注意必须在 finally
语句块中释放锁,否则当出现异常时,获取到的锁永远不会被释放,在代码中存在这种情况无疑是一种定时炸弹。
显示锁相较于内置锁还提供了等待可中断、公平与非公平等功能,当然还有一个优点是显示锁更加灵活。
PS:
这篇博文中很多知识点拿出来都可以单独写一篇文章,这里只是总结了一部分重要的知识点,先让大家对多线程有一个感性的认识。后面会陆续的补充并发编程系列的文章。如果大家觉得写得还行的话,请持续关注后面的文章。
参考资料
《Java 并发编程实战》
《深入理解 Java 虚拟机》
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。