概述
- 使用锁是为了解决安全问题
- 使用并发主要是让线程既安全,又效率
- 安全问题主要是由于内存的是否被安全使用了
- 效率问题主要考虑的是线程是否被cpu合理调用了
经测试,当并发执行累加操作不超过百万次时,并行速度会比串行化执行累加操作要慢。主要原因是因为并行会导致更多的线程的上下文切换 ```java public class TestThread { static final long count =1000010000; public static void main(String[] args) throws InterruptedException {
concurrency(); //多线程执行
serial();//单线程执行
}
static void concurrency() throws InterruptedException {
long start = System.currentTimeMillis();
Thread thread = new Thread(()->{
int a =0;
for(long i=0;i<count;i++){
a+=5;
}
});
thread.start();
int b =0;
for(long i=0;i<count;i++){
b-=5;
}
long end =System.currentTimeMillis();
thread.join();//确保thread线程执行完毕
System.out.println("多线程使用时间:"+(end-start));
}
//单线程执行 static void serial(){
long start = System.currentTimeMillis();
int a =0;
for(long i=0;i<count;i++){
a+=5;
}
int b =0;
for(long i=0;i<count;i++){
b-=5;
}
long end =System.currentTimeMillis();
System.out.println("单线程使用时间:"+(end-start));
}
}
- 减少上下文切换的方式
- 无锁并发编程
- CAS算法
- 使用最少线程,通过使用线程池控制线程的数量
- 协程:比线程还要小调用cpu的单位
<a name="L1O3r"></a>
## Volatile
- volatile是jdk提供一个关键字,可作为轻量级的锁,不会引起上下文件线程的切换
- volatile的作用
- 保证线程的可见性
- 防止指令重排
- volatile可以一定程度解决并发问题,但是遇到原子性导致的并发就无法解决
- 想要保证线程安全需要解决以下三个问题
- 原子性
- 可见性
- 有序性
- volatile只能修饰成员变量,不能修饰局部变量
<a name="YRhuV"></a>
## 线程可见性问题
- 可见性代码问题
```java
public static class RecoderExample {
volatile int a=0;
//实现了获取a的值的方法
public void get(){
int local_value = a;
while (local_value < 15) {
if (local_value != a) {
System.out.println("获取a的值 : " + a);
local_value = a;
}
}
}
//修改a的值
public void change(){
while (a < 15) {
System.out.println("新增之后的a的值 " + (a + 1));
a++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
RecoderExample recoderExample = new RecoderExample();
//创建一个获取值的线程
Thread t1 = new Thread(()->{
recoderExample.get();
});
//创建一个线程修改值
Thread t2 = new Thread(()->{
recoderExample.change();
});
t1.start();
t2.start();
}
- 为什么会有可见性的问题?
- Java共享内存模型JMM的机制
- java中所有的变量是存储在主内存中的
- 每个线程要去修改主内存中的变量的时候,是将主内存中的变量复制一份到线程的本地内存中进行修改
- 然后线程修改变量成功之后,将线程的本地的内存的变量再刷新到主内存中
- 在多线程编程的时候,可能有几个线程同时将一个变量值复制到本地内存中然后进行修改,这样就导致了并发问题
- 所以线程中的变量默认情况下是不可见的
- Java共享内存模型JMM的机制
在使用了volatile修饰变量之后,当一个线程的本地变量被修改了,其他的变量都是可以见的
线程操作的原子性
所谓的原子性是不可分割的操作
- volatile不可以保证操作的原子性,synchronized可以保证
- volatile用于信号灯的操作,即一个线程改,多个线程查询等操作
可以使用JUC中提供的原子类解决原子性的问题
public class TestVolatile002 {
int count =0;
AtomicInteger atomicInteger = new AtomicInteger(0);
void m() {
for (int i = 0; i < 10000; i++) {
count++;
atomicInteger.incrementAndGet();
}
}
public static void main(String[] args) {
TestVolatile002 t = new TestVolatile002();
List<Thread> threads = new ArrayList<>();
//创建10个线程
for (int i = 0; i < 10; i++) {
threads.add( new Thread(t::m,"thread"+i));
}
//启动10个线程
threads.forEach((o)->o.start());
//当前主线程等待所有的线程完成
threads.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
System.out.println(t.count);
System.out.println("原子操作的值:"+t.atomicInteger.get());
}
}
线程操作的顺序性
java的编译器为了提高程序运行的效率,在编译时候会将源码的顺序进行重排,以便提高程序运行的效率
int x = 10; int x = 10;
int y = 9; x = x+10
x = x+10; int y=9;
在单线程的情况下问题不大,但是在多线程的情况有可能导致并发的问题
public class TestVolatile003 {
public static int a = 0,b=0;
public static int i = 0,j=0;
public static void main(String[] args) throws InterruptedException {
int cnt=0;
while(true){
a = 0;
b=0;
i=0;
j=0;
Thread t1 = new Thread(()->{
a=1;
i=b;
});
Thread t2 = new Thread(()->{
b=1;
j=a;
});
t1.start();
t2.start();
t1.join();
t2.join();
cnt++;
System.out.println("第"+cnt+"次执行,i="+i+",j="+j);
if(i==0&& j==0){
break;
}
}
}
}
在单例模式(Double Check Lock)解决指令重排的问题
public class MySingleton {
static volatile MySingleton instance;
List list;
private MySingleton(){
list = new ArrayList();//这个代码可能会在赋值之后执行
}
public int getNum(){
return list.size();
}
public static MySingleton getInstance(){
if(instance == null){//由于指令重排instance已经不为空,但是构造方法没有被调用
synchronized(MySingleton.class){
if(instance==null){
instance = new MySingleton();
}
}
}
return instance;
}
}
new的正常顺序
- 1内存为对象分配空间
- 2调用构造方法
- 3将结果返回的引用
由于指令重排之后以上步骤变成一下情况
- 1内存为对象分配空间
- 3将结果返回的引用
- 2调用构造方法
volatile 和 synchronized
1、volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
2、volatile保证数据的可见性,但是不保证原子性; 而synchronized是一种排他(互斥)的机制,既保证可见性,又保证原子性。
3、volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
4、volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。CAS算法
传统的锁会在加锁,或是释放的时候导致过多的上下文切换
CAS算法:compareAndSwap
public final int incrementAndGet() {
for (;;) {
int current = get();//获取当前的变量的值
int next = current + 1; //预判操作
if (compareAndSet(current, next))//判断预操作是否成功
return next; //成功则返回
//不成功则继续
}
}
通过以上的算法,线程就不会轻易的释放cpu资源,从而减少了上下文的切换
- CAS算法主要是通过Unsafe类实现
在CAS的基础上出现快速解决并发问题的各种工具包
JUC(原子包),提供各种类型原子操作类
AtomicBoolean:原子更新布尔类型。
AtomicInteger:原子更新整型。
AtomicLong:原子更新长整型。
AtomicInteger
QAS(各种支持并发的集合)
- ArrayList并不是线程安全的,Vector是线程安全的,但是Vector使用synchronized
- CopyOnWriteArrayList是线程安全的,且效率比Vector高
- Hashmap并不是线程安全的,
- ConcurrentHashMap是线程安全
- 主要使用了分段锁进行实现
- ConcurrentLinkedQueue:线程安全的队列
阻塞队列的使用
public class TestBlockQueue {
static AtomicInteger cnt = new AtomicInteger(0);
public static void main(String[] args) {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue(5);
//多个消费则
List<Thread> products = new ArrayList<>();
for(int i=0;i<3;i++){
products.add(new Thread(()->{
String name = Thread.currentThread().getName();
for(int j=0;j<5;j++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
// System.out.println(name+"做的食品"+(cnt.get()+1));
queue.put(name+"做的食品"+cnt.incrementAndGet()); //put是阻塞的方法
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}));
}
//启动线程
products.forEach(thread -> {
thread.start();
});
//创建消费者
new Thread(()->{
for(int i=0;i<5;i++){
String food= null;//出队列
try {
food = queue.take();//获取队列中元素
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费1食品:"+food);
}
}).start();
new Thread(()->{
for(int i=0;i<5;i++){
String food= null;//出队列
try {
food = queue.take();//获取队列中元素
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费2食品:"+food);
}
}).start();
new Thread(()->{
for(int i=0;i<5;i++){
String food= null;//出队列
try {
food = queue.take();//获取队列中元素
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费3食品:"+food);
}
}).start();
}
}
- ArrayList并不是线程安全的,Vector是线程安全的,但是Vector使用synchronized