线程安全问题
- 当使用多个线程去完成同一个任务时,就有可能照成线程安全问题
示例:
class MyThread implements Runnable{
int num = 100;
@Override
public void run() {
while(true){
if(num < 1){
break;
}
System.out.println(Thread.currentThread().getName() + "买出了第" +num + "张票");
num--;
}
}
}
public class Text {
public static void main(String[] args) {
new Thread(new MyThread(),"窗口1").start();
new Thread(new MyThread(),"窗口2").start();
// 会照成重复票,不存在的票,和遗漏票
}
}
会照成重复票,不存在的票,和遗漏票
原因: 线程的调度是抢占式
解决办法: 加锁
synchronized锁
-
synchronized关键字:表示“同步”的。它可以对“多行代码”进行“同步”——将多行代码当成是一个完整的整体,一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行
-
synchronized两种使用方式:
- 同步代码块
- 同步方法(常用)
同步代码块_
- 使用synchronized关键字修饰的代码块就是同步代码块,表示只对这个区块的资源实行互斥访问
class MyThread implements Runnable{
int num = 100;
@Override
public void run() {
while(true){
//使用同步锁 多个线程之间的锁对象要一致
synchronized (this) {// 加锁
if(num < 1){
break;
}
System.out.println(Thread.currentThread().getName() + "买出了第" +num + "张票");
num--;
}// 释放锁
}
}
}
public class Text {
public static void main(String[] args) {
MyThread mt = new MyThread();
// 创建两条线程
new Thread(mt,"窗口1").start();
new Thread(mt,"窗口2").start();
}
}
同步方法_
- 使用synchronized关键字修饰方法就是同步方法,表示整个方法的资源实行互斥访问
非静态同步方法锁对象是: this
静态同步方法锁对象是: 该方法所在类的字节码对象:类名.class
非静态的同步方法锁:
class MyThread implements Runnable{
int num = 100;
@Override
public void run() {
while (true){
if(Threadfunc())break;
}
}
// 非静态同步方法锁
private synchronized boolean Threadfunc() {
//使用同步锁 多个线程之间的锁对象要一致
synchronized (this) { // 加锁
if(num < 1){
return true;
}
System.out.println(Thread.currentThread().getName() + "买出了第" +num + "张票");
num--;
return false;
}
}
}
public class Text {
public static void main(String[] args) {
MyThread mt = new MyThread();
// 四个线程同时买票
new Thread(mt,"窗口1").start();
new Thread(mt,"窗口2").start();
new Thread(mt,"窗口3").start();
new Thread(mt,"窗口4").start();
}
}
静态同步方法锁:
public class Text {
// 创建全局变量
static int num = 100;
public static void main(String[] args) {
Thread th = new Thread(new Runnable() {
@Override
public void run() {
while (true){
if(Text.ThreadFunc())break;
}
}
});
// 创建二条线程
new Thread(th,"窗口1").start();
new Thread(th,"窗口2").start();
}
// 静态同步方法锁 它的锁对象是 类名.class
public static synchronized boolean ThreadFunc() {
if (num < 1) {
return true;
}
System.out.println(Thread.currentThread().getName() + "买了第" + num + "张票");
num--;
return false;
}
}
Lock锁
- 也是一种锁,他比synchronized更加强大,更加面向对象
- Lock是一个接口,Lock
实现提供了比使用synchronized
方法和语句可获得的更广泛的锁定操作
- 使用Lock就需要使用Lock接口的实现类ReentrantLock
常用方法_
void lock();加锁
void unlock();释放锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Text {
public static void main(String[] args) {
// 创建锁对象
Lock lock = new ReentrantLock();
Thread th = new Thread(new Runnable() {
int num = 100;
@Override
public void run() {
while (true){
// 加锁
lock.lock();
if(num < 1){
lock.unlock(); // 最后一次退出前还要释放就次
break;
}
System.out.println(Thread.currentThread().getName() + "买出了第" + num + "张票");
num--;
//释放锁
lock.unlock();
}
}
});
// 创建两个线程
new Thread(th,"窗口1").start();
new Thread(th,"窗口2").start();
}
}
高并发及线程安全
- 高并发:是指在某个时间点上,有大量的用户(线程)同时访问同一资源 例如:天猫的双11购物节、12306的在线购票在某个时间点上,都会面临大量用户同时抢购同一件商品/车票的情况
- 线程安全:在某个时间点上,当大量用户(线程)访问同一资源时,由于多线程运行机制的原因,可能会导致被访问的资源出现"数据污染"的问题
多线程的安全性问题 -可见性_
-
原因:子线程对共享变量值的修改对主线程不可见 (一条线程对共享变量的修改,其他线程不可见)
解决方案:Volatile关键字
(它是一个修饰符,只能用来修饰成员变量
) -
被volatile修饰的成员变量,可以强制要求线程从主内存中获取新的值
-
被volatile修饰的成员变量,可以保证不会被编译器重排
-
volatile可以解决可见性,有序性问题,但是不能解决原子性问题
多线程的安全性问题 - 有序性
- 原因:有些时候“编译器”在编译代码时,会对代码进行“重排”
解决方案:Volatile关键字
多线程的安全性问题 - 原子性
-原因: 所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体
解决方案:加锁
或使用AtomicInteger类
- java.util.concurrent.atomic包中提供了很多原子类,这些原子类在多线程的环境下是线程安全的
作用: 可以解决原子性问题,可见性问题,有序性问题
- AtomicInteger类:
-public AtomicInteger();创建一个AtomicInteger对象,表示整数0
-public AtomicInteger(int nul);创建一个AtomicInteger对象,表示指定整数
-public final int getAndIncrement(); 自增1
-public final int get(); 获取当前对象表示的整数值
import java.util.concurrent.atomic.AtomicInteger;
public class Text2 {
// 创建一个全局对象
static AtomicInteger a = new AtomicInteger(0);
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
// 自增1
a.getAndIncrement();
}
}
}).start();
for (int i = 0; i < 100000; i++) {
// 自增1
a.getAndIncrement();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取当前对象表示的整数值
System.out.println(a.get()); // 输出 200000
}
}
AtomicIntegerArray类
-
常用的数组操作的原子类_
- java.util.concurrent.atomic.AtomicIntegerArray:对int数组操作的原子类 int[]
- java.util.concurrent.atomic.AtomicLongArray:对long数组操作的原子类 long[]
- java.util.concurrent.atomic.AtomicReferenceArray:对引用类型数组操作的原子类 Object[] -
构造方法_
-public AtomicIntegerArray(int length) 创建AtomicIntegerArray对象并指定数组长度
-
常用方法_
-public final int getAndAdd(int i,int delta);为指定索引的元素添加数据
-public int length();返回数组的长度
import java.util.concurrent.atomic.AtomicIntegerArray;
public class Text3 {
// 创建一个长度为500的数组对象
static AtomicIntegerArray arr = new AtomicIntegerArray(500);
public static void main(String[] args) {
Thread th = new Thread(new Runnable() {
@Override
public void run() {
//
for (int i = 0; i < arr.length(); i++) {
arr.addAndGet(i,1);
}
}
});
for (int i = 0; i < 1000; i++) {
new Thread(th).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数组:" + arr);
// 输出 500个 1000
}
}
并发包
- 在多线程开发中,进行多线程操作时,集合中的元素可能并不安全,这时就可以采用线程更安全的集合
ArrayList 可以换成 CopyOnWriteArrayList
HashSet 可以换成 CopyOnWriteArraySet
HashMap 可以换成 ConcurrentHashMap
HashTable效率低下原因: 使用全局synchronized锁
CountDownLatch
- CountDownLatch允许一个或多个线程等待其他线程完成操作
例如:线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后,要线程2打印B之后才能打印C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行
-
构造方法_
-public CountDownLatch(int count) 初始化一个指定计数器的CountDownLatch对象
-
重要方法_
-public void await() 让当前线程等待
-public void countDown() 计数器进行减1
示例:
import java.util.concurrent.CountDownLatch;
public class Text4 {
// 创建全局对象
static CountDownLatch cdl = new CountDownLatch(1);
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程1:打印A");
try {
// 进入等待
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1:打印C");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程2:打印B");
// 打印完之后 计数器 -1
cdl.countDown();
}
}).start();
// 输出
// 线程1:打印A
//线程2:打印B
//线程1:打印C
}
}
CyclicBarrier
**作用:**让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行
- 常用方法_
- public CyclicBarrier(int parties, Runnable barrierAction)parties: 代表要达到屏障的线程数量
barrierAction:表示所有线程都达到屏障后要执行的任务
- public int await() 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
示例:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class Text5 {
public static void main(String[] args) {
CyclicBarrier cb = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("线程全部到齐");
System.out.println("线程离开");
}
});
Thread th = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "到达");
// 五个线程到达才能执行下一条命令
try {
cb.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "离开");
}
});
new Thread(th,"线程1").start();
new Thread(th,"线程2").start();
new Thread(th,"线程3").start();
new Thread(th,"线程4").start();
new Thread(th,"线程5").start();
// 输出
// 线程1到达
//线程3到达
//线程2到达
//线程4到达
//线程5到达
//线程全部到齐
//线程离开
//线程3离开
//线程1离开
//线程2离开
//线程5离开
//线程4离开
}
}
Semaphore
作用: Semaphore的主要作用是控制线程的并发数量
- 常用方法_
- public Semaphore(int permits) permits 表示许可线程的数量
- public void acquire() 表示获取许可
- public void release() 表示释放许可
示例:
import java.util.concurrent.Semaphore;
public class Text6 {
public static void main(String[] args) {
// 每次只允许两条线程进入
Semaphore sp = new Semaphore(2);
Thread th = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "进来了");
try {
sp.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "忙碌中");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "出去了");
sp.release();
}
});
new Thread(th,"路人A").start();
new Thread(th,"路人B").start();
new Thread(th,"路人C").start();
new Thread(th,"路人D").start();
new Thread(th,"路人E").start();
}
}
Exchanger
作用: 是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换
-
构造方法
- public Exchanger() -
常用方法
- public V exchange(V x) 参数就表示当前线程需要传递的数据,返回值是其他线程传递过来的数据
import java.util.concurrent.Exchanger;
public class Text7 {
public static void main(String[] args) {
Exchanger<String> ec = new Exchanger();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "把123传递给线程B");
try {
String msgB = ec.exchange("123");
System.out.println(Thread.currentThread().getName() +"B线程传递过来的数据是" + msgB);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程A:").start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "把456传递给线程A");
try {
String msgA = ec.exchange("456");
System.out.println(Thread.currentThread().getName() +"A线程传递过来的数据是" + msgA);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程B:").start();
// 输出
//线程A:把123传递给线程B
//线程B:把456传递给线程A
//线程B:A线程传递过来的数据是123
//线程A:B线程传递过来的数据是456
}
}
线程池
**线程池:**其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源
-
线程池的好处_
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下 -
线程池的使用
- Executors线程池工具类 (里面提供了一些静态方法, 可以用来生成一些常用的线程池)
public static ExecutorService newFixedThreadPool(int nThreads)获取线程池指定线程数量
- 使用线程池: ExecutorService线程池接口
-public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行任务
-public <T> Future<T> submit(Callable<T> task):获取线程池中的某一个线程对象,并执行任务
Future用来封装返回值,封装后的返回值可以通过Future的get()方法获取
对于线程池提交的任务是实现Runnable的任务,那么这个返回值Future其实没有啥用处
对于线程池提交的任务是实现Callable的任务,那么这个返回值Future就有用
因为Callable的任务方法:V call()
有返回值,执行完call方法的返回值会封装成Future对象返回,如果想要得到call方法的返回值,就通过Future对象调用get方法得到
线程池的使用步骤:
- 创建线程池
- 提交任务
- 销毁线程池(一般不操作)
示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Text8 {
public static void main(String[] args) {
// 创建线程池,指定初始化数量
ExecutorService es = Executors.newFixedThreadPool(2);
Thread th = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始执行任务...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束任务...");
}
});
// 提交任务
es.submit(th);
es.submit(th);
es.submit(th);
es.submit(th);
es.submit(th);
es.submit(th);
// 销毁线程池(一般不操作)
es.shutdown();
}
}