目录
前言
一、Java并发编程(JUC)是什么?
二、Java创建多线程的4种方式
1.继承Thread类
2.实现Runnable接口
3.实现Callable接口
4.线程池
三、sychornized与volatile
四、多线程锁——JUC中的类(Lock)
1.ReentrantLock 可重入锁
2.ReadWriteLock 读写锁
3.Lock 和 synchronized 的不同
五、线程间的通信——信号量机制
1.synchronized 方案(wait、notify)
2.Lock 方案(await,signal)
七、死锁与解决方法
1.死锁
2.解决方法
3.验证是否死锁
六、集合中的线程安全
总结
随着多核cpu的出现,以及为了程序运行的效率,多线程的编程技术越来越重要。在多线程编程中最重要的就是对共享资源的并发访问。如何能安全,高效,互不干扰的让各个线程稳定运行成为多线程编程中的关键。
Java并发编程是基于多线程技术的一种编程技术,该技术是为了解决资源利用率、响应速度、线程安全而创建的,能极大的提高程序的运行效率。JUC是指java.util.concurrent这个jdk自带的包的简称,这个包下有Java5发布的一系列新的关于并发操作的类,极大方便了我们对并发编程的实现。
代码如下(示例):
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() + "结束了");
}
}
代码如下(示例):
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() + "结束了");
}
代码如下(示例):
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;
}
}
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() + " 结束了");
}
相比其他方式,线程池的优势:
代码如下(示例):
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 {
@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(), //阻塞队列
Executors.defaultThreadFactory(), //线程工厂,用于创建线程
new ThreadPoolExecutor.AbortPolicy()); //拒绝策略
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
Future submit = service.submit(new NumberThread1());//适合使用于Callable
System.out.println(submit.get());
//3.关闭连接池
service.shutdown();
}
}
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线程中也可以获取到)。
- 缺点是:不具备互斥性、不保证原子性
//第一步 创建资源类,定义属性和和操作方法
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())。
//资源类
class MyCache {
private volatile Map 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();
}
}
}
- 如果有一个线程已经占用了读锁,其他线程也可以申请读锁。(读锁之间是共享锁)
- 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。 (读锁与写锁是互斥锁)
- 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。(写锁与写锁是互斥锁)
线程间通信是指:线程间因需要协同工作、按照正确的流程去执行,相互传达信号的过程。最常见的线程通信就是唤醒指定线程。
//第一步 创建资源类,定义属性和操作方法
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();
}
}
//第一步 创建资源类
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更灵活,可以实现线程间的定制化通信
演示死锁:
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();
}
}
#找到Java进程
jps -l
#执行打印线程堆栈信息
jstack 进程号
如果你查询的Java进程出现了死锁,堆栈信息的最后一行会有相关提示。
public class ThreadDemo4 {
public static void main(String[] args) {
//演示ArrayList集合
// List list = new ArrayList<>();//线程不安全
// Vector解决
// List list = new Vector<>();
//Collections解决
// List list = Collections.synchronizedList(new ArrayList<>());
// CopyOnWriteArrayList解决
List 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 set = new HashSet<>();//线程不安全
Set 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 map = new HashMap<>();//线程不安全
Map 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算法、一些辅助类、还有各种锁的概念、等等。