目录
1.synchronized 方案(wait、notify)
前言
随着多核cpu的出现,以及为了程序运行的效率,多线程的编程技术越来越重要。在多线程编程中最重要的就是对共享资源的并发访问。如何能安全,高效,互不干扰的让各个线程稳定运行成为多线程编程中的关键。
一、Java并发编程(JUC)是什么?
Java并发编程是基于多线程技术的一种编程技术,该技术是为了解决资源利用率、响应速度、线程安全而创建的,能极大的提高程序的运行效率。JUC是指java.util.concurrent这个jdk自带的包的简称,这个包下有Java5发布的一系列新的关于并发操作的类,极大方便了我们对并发编程的实现。
二、Java创建多线程的4种方式
1.继承Thread类
代码如下(示例):
public class Main {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
myThread1.start();
System.out.println(Thread.currentThread().getName() + "结束了");
}
}
class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
System.out.println(Thread.currentThread().getName() + "结束了");
}
}
2.实现Runnable接口
代码如下(示例):
public class Main {
public static void main(String[] args) {
MyThread2 myThread2 = new MyThread2();
Thread thread = new Thread(myThread2);
thread.start();
System.out.println(Thread.currentThread().getName() + "结束了");
}
}
class MyThread2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
System.out.println(Thread.currentThread().getName() + "结束了");
}
}
注意,Runnable是函数式接口,配合Lambda表达式可以写成更简洁的代码。示例:
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
System.out.println(Thread.currentThread().getName() + "结束了");
}, "线程1");
thread.start();
System.out.println(Thread.currentThread().getName() + "结束了");
}
3.实现Callable接口
代码如下(示例):
public class Main {
public static void main(String[] args) {
NumThread numThread = new NumThread();
// 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象。
FutureTask futureTask = new FutureTask(numThread);
// FutureTask也实现了Runnable接口,所以其实例可以传递到Thread构造器中。
new Thread(futureTask).start();
System.out.println(Thread.currentThread().getName() + " 想要获取返回值");
Object sum = null;
try {
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。注意,此处主线程会阻塞
sum = futureTask.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
System.out.println("总和为:" + sum);
System.out.println(Thread.currentThread().getName() + " 结束了");
}
}
class NumThread implements Callable {
@Override
public Object call() throws Exception {
Integer sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + "结束了");
return sum;
}
}
- Callable是Java5新增的创建多线程的接口
- 注意,Callable也是函数式接口,配合Lambda表达式可以写成更简洁的代码。示例:
public static void main(String[] args) {
FutureTask futureTask = new FutureTask(() -> {
Integer sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + "结束了");
return sum;
});
// FutureTask也实现了Runnable接口,所以其实例可以传递到Thread构造器中。
new Thread(futureTask, "线程1").start();
System.out.println(Thread.currentThread().getName() + " 想要获取返回值");
Object sum = null;
try {
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。注意,此处会主线程会阻塞
sum = futureTask.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
System.out.println("总和为:" + sum);
System.out.println(Thread.currentThread().getName() + " 结束了");
}
4.线程池
相比其他方式,线程池的优势:
- 提高响应速度(减少了创建新线程的时间)。
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)。
- 便于线程管理。
代码如下(示例):
class NumberThread implements Runnable {
@Override
public void run() {
for (int i = 0; i <= 10; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Callable<Integer> {
@Override
public Integer call() {
for (int i = 0; i <= 10; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
return 100;
}
}
public class ThreadPool {
public static void main(String[] args) throws Exception {
//1. 提供指定线程数量的线程池
ExecutorService service = new ThreadPoolExecutor(5, //线程池的核心线程数
10, //最大线程数
0L, //多余空闲线程的存活时间
TimeUnit.MILLISECONDS, //存活时间的单位
new LinkedBlockingQueue<Runnable>(), //阻塞队列
Executors.defaultThreadFactory(), //线程工厂,用于创建线程
new ThreadPoolExecutor.AbortPolicy()); //拒绝策略
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
Future<Integer> submit = service.submit(new NumberThread1());//适合使用于Callable
System.out.println(submit.get());
//3.关闭连接池
service.shutdown();
}
}
三、sychornized与volatile
- sychornized:修饰代码块或方法;是一种线程同步机制,也可被称为可重入锁。
- volatile:修饰变量;在Java并发编程中常用于保持内存可见性和防止指令重排序;volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,不能保证原子性。是比sychornized更轻量级的同步机制。
- 它们是jdk初始版本就存在的关键字。
sychornized代码如下(示例):
class Window1 implements Runnable {
private int ticket = 100; //共享变量
@Override
public void run() {
while (true) {
synchronized (this) {//此时的this(同步监视器):唯一的Window1的对象
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w = new Window1();
Thread t1 = new Thread(w, "窗口1");
Thread t2 = new Thread(w, "窗口2");
t1.start();
t2.start();
}
}
- synchronized包裹的代码,即为需要被同步的代码。(不能包含代码多了,也不能包含代码少了)。
- 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
- 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
- synchronized的好处是解决了线程安全问题。
- 坏处是同步代码块或同步方法同一时间只能有一个线程参与执行,其他线程等待,相当于是一个单线程过程,效率低。
volatile代码如下(示例):
public class VolatileDemo implements Runnable {
private static volatile boolean flag = false;//注意只有加上volatile关键字程序才能正常停止
// private static boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag=" + flag);
}
public static void main(String[] args) {
VolatileDemo vd = new VolatileDemo();
new Thread(vd, "线程1").start();
while (true) {
if (flag) {
System.out.println("==================");
break;
}
}
}
}
- 当多个线程进行操作共享数据时,可以保证内存中的数据可见(线程1中修改了flag的值,main线程中也可以获取到)。
- 缺点是:不具备互斥性、不保证原子性
四、多线程锁——JUC中的类(Lock)
1.ReentrantLock 可重入锁
//第一步 创建资源类,定义属性和和操作方法
class LTicket {
private int number = 30;//票数量
private final ReentrantLock lock = new ReentrantLock(true);//创建可重入锁;true表示公平锁
//卖票方法
public void sale() {
lock.lock();//上锁
try {
if (number > 0) { //判断是否有票
System.out.println(Thread.currentThread().getName() + " :卖出" + (number--) + " 剩余:" + number);
}
} finally {
lock.unlock();//解锁
}
}
}
public class LSaleTicket {
//第二步 创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
LTicket ticket = new LTicket();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "线程1").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "线程2").start();
}
}
synchronized与Lock使用方式的异同?
相同:二者都可以解决线程安全问题,都是可重入锁。
不同:synchronized机制在执行完相应的同步代码以后,自动释放同步监视器;Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())。
2.ReadWriteLock 读写锁
//资源类
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();//创建map集合
private ReadWriteLock rwLock = new ReentrantReadWriteLock();//创建读写锁对象
//放数据
public void put(String key, Object value) {
rwLock.writeLock().lock();//添加写锁
try {
System.out.println(Thread.currentThread().getName() + " 正在写操作" + key);
TimeUnit.MICROSECONDS.sleep(300); //暂停一会
map.put(key, value); //放数据
System.out.println(Thread.currentThread().getName() + " 写完了" + key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock(); //释放写锁
}
}
//取数据
public Object get(String key) {
rwLock.readLock().lock(); //添加读锁
Object result = null;
try {
System.out.println(Thread.currentThread().getName() + " 正在读取操作" + key);
TimeUnit.MICROSECONDS.sleep(300);//暂停一会
result = map.get(key);
System.out.println(Thread.currentThread().getName() + " 取完了" + key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();//释放读锁
}
return result;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) throws InterruptedException {
MyCache myCache = new MyCache();
//创建线程放数据
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(() -> {
myCache.put(num + "", num + "");
}, "写线程" + String.valueOf(i)).start();
}
TimeUnit.MICROSECONDS.sleep(300);
//创建线程取数据
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(() -> {
myCache.get(num + "");
}, "读线程" + String.valueOf(i)).start();
}
}
}
- 如果有一个线程已经占用了读锁,其他线程也可以申请读锁。(读锁之间是共享锁)
- 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。 (读锁与写锁是互斥锁)
- 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。(写锁与写锁是互斥锁)
3.Lock 和 synchronized 的不同
- Lock 是一个接口,而 synchronized 是 ava 的关键字,synchronized 是内置的语言实现。
- synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现 象发生;而 Lock 在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。
- Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
- Lock 可以提高多个线程进行读操作的效率。在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。
五、线程间的通信——信号量机制
线程间通信是指:线程间因需要协同工作、按照正确的流程去执行,相互传达信号的过程。最常见的线程通信就是唤醒指定线程。
1.synchronized 方案(wait、notify)
//第一步 创建资源类,定义属性和操作方法
class Share {
private int number = 0;//初始值
//+1的方法
public synchronized void incr() throws InterruptedException {
//第二步 判断 干活 通知
while (number != 0) { //判断number值是否是0,如果不是0,等待
this.wait(); //在哪里睡,就在哪里醒
}
number++;//如果number值是0,就+1操作
System.out.println(Thread.currentThread().getName() + " :: " + number);
this.notifyAll();//通知其他线程
}
//-1的方法
public synchronized void decr() throws InterruptedException {
while (number != 1) {//判断
this.wait();
}
number--;//干活
System.out.println(Thread.currentThread().getName() + " :: " + number);
this.notifyAll();//通知其他线程
}
}
public class ThreadDemo1 {
//第三步 创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
Share share = new Share();
//创建线程
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
share.incr(); //+1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AA").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
share.decr(); //-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "BB").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
share.incr(); //+1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "CC").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
share.decr(); //-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "DD").start();
}
}
2.Lock 方案(await,signal)
//第一步 创建资源类
class ShareResource {
//定义标志位
private int flag = 1; // 1 AA 2 BB 3 CC
//创建Lock锁
private Lock lock = new ReentrantLock();
//创建三个condition
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
//打印5次,参数第几轮
public void print5(int loop) throws InterruptedException {
lock.lock();//上锁
try {
while (flag != 1) {//判断
c1.await();//等待
}
for (int i = 1; i <= 5; i++) {//干活
System.out.println(Thread.currentThread().getName() + " :: " + i + " :轮数:" + loop);
}
//通知
flag = 2; //修改标志位 2
c2.signal(); //通知BB线程
} finally {
lock.unlock();//释放锁
}
}
//打印10次,参数第几轮
public void print10(int loop) throws InterruptedException {
lock.lock();
try {
while (flag != 2) {
c2.await();
}
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " :: " + i + " :轮数:" + loop);
}
flag = 3;//修改标志位
c3.signal();//通知CC线程
} finally {
lock.unlock();
}
}
//打印15次,参数第几轮
public void print15(int loop) throws InterruptedException {
lock.lock();
try {
while (flag != 3) {
c3.await();
}
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + " :: " + i + " :轮数:" + loop);
}
flag = 1;//修改标志位
c1.signal();//通知AA线程
} finally {
lock.unlock();
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
shareResource.print5(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AA").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
shareResource.print10(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "BB").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
shareResource.print15(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "CC").start();
}
}
通过上面的示例,可以看出Lock比synchronized更灵活,可以实现线程间的定制化通信
七、死锁与解决方法
1.死锁
- 死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
演示死锁:
public class DeadLock {
//创建两个对象
static Object a = new Object();
static Object b = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (a) {
System.out.println(Thread.currentThread().getName() + " 持有锁a,试图获取锁b");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println(Thread.currentThread().getName() + " 获取锁b");
}
}
}, "A").start();
new Thread(() -> {
synchronized (b) {
System.out.println(Thread.currentThread().getName() + " 持有锁b,试图获取锁a");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a) {
System.out.println(Thread.currentThread().getName() + " 获取锁a");
}
}
}, "B").start();
}
}
2.解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
3.验证是否死锁
#找到Java进程
jps -l
#执行打印线程堆栈信息
jstack 进程号
如果你查询的Java进程出现了死锁,堆栈信息的最后一行会有相关提示。
六、集合中的线程安全
- 我们知道java中的常用集合类ArrayList,Hashset,和HashMap都是线程不安全的。
- 当有多个线程同时向这三个集合写入数据是就会出现java.util.ConcurrentModificationException 这个异常信息
- 这时,我们就需要使用线程安全的集合来替代。
public class ThreadDemo4 {
public static void main(String[] args) {
//演示ArrayList集合
// List<String> list = new ArrayList<>();//线程不安全
// Vector解决
// List<String> list = new Vector<>();
//Collections解决
// List<String> list = Collections.synchronizedList(new ArrayList<>());
// CopyOnWriteArrayList解决
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
//向集合添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
//从集合获取内容
System.out.println(list);
}, String.valueOf(i)).start();
}
//演示Hashset
// Set<String> set = new HashSet<>();//线程不安全
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
//向集合添加内容
set.add(UUID.randomUUID().toString().substring(0, 8));
//从集合获取内容
System.out.println(set);
}, String.valueOf(i)).start();
}
//演示HashMap
// Map<String,String> map = new HashMap<>();//线程不安全
Map<String, String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 30; i++) {
String key = String.valueOf(i);
new Thread(() -> {
//向集合添加内容
map.put(key, UUID.randomUUID().toString().substring(0, 8));
//从集合获取内容
System.out.println(map);
}, String.valueOf(i)).start();
}
}
}
如果,要使用线程安全的集合:推荐CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap这三个。它们都是java.util.concurrent包下的类,在保证线程安全的前提下,也有不错的效率。
总结
Java并发编程是我们工作中出现问题最多的地方,也是面试的高频考点。本文仅作简单介绍,Java并发线程的知识远不止这些,还有:分支合并框架、异步回调、CAS算法、一些辅助类、还有各种锁的概念、等等。