什么是并发
并发在我理解就是,一段代码被多个线程同时执行,而出现的不可预期的问题
什么情况下会出现并发问题
当一个变量是全局变量,并且可能同时被多个线程访问到的情况下就会出现并发问题,例如一个类的全局变量,同时修改数据库中的某一个字段等。
怎么解决并发问题
解决并发问题就是:使得被多线程访问的代码具有,原子性,可见性和顺序性
这种解决方式就是将全局变量局部化,使得不同的线程操作的是不同的线程,这样就不会出现多线程同时访问一个变量导致的并大问题了。因为在jVM中局部变量因为在方法中,而方法的执行实在栈帧中,栈帧是单线程独享区域,是天然就具备线程安全的。
但是这样会有一个问题就是会浪费大量的资源,因为Java的栈是非常小的一块区域,这样很容易出现内存溢出。其次有些数据无法局部化,比如同时修改数据库中的某一个字段。
Java的同步机制最基础的就包括:synchronized关键词。synchronized关键词可以修饰类,方法和代码块使用起来很方便,而且相比其他的同步方法(后面会有介绍)没有锁没有释放的风险,因为synchronized是基于JVM的锁机制,JVM会自动的释放锁。
缺点就是这个锁比较‘重’,怎么说呢,很明显这样的方式使得所有的代码都穿行执行了,并发量肯定就无了啊,就和单线程执行一样,好在JDK1.6开始对该关键词做了优化,加入锁升级的概论:无锁->偏向锁->轻量级锁->重量级锁
public class SyncTest{
//代码块
synchronized(SyncTest.class){
//todo 同步代码
}
/**
*方法
**/
public void test(){
synchornized(SyncTest.class){
// todo 同步代码
}
}
}
使用类加锁,不管有多少个对像,而锁只有一个,那个线程先拿到锁就执行这部分代码,其他线程阻塞。
除了使用类加锁是类级锁之外,如静态方法也是类级。众所周知,静态方法属于类,而不属于具体的某一个对象
代码:
public class SyncTest{
public synchronized static void test(){
//todo 同步代码
}
}
public class SyncTest{
//代码块
synchronized(this){
//todo 同步代码
}
/**
*方法
**/
public void test(){
synchornized(this){
// todo 同步代码
}
}
}
使用得是’this’关键词加锁,this指当前对象,所以锁住的只是当前的对象,如果有多个对象就有多个锁
代码
public class SyncTest{
/**
*方法
**/
public synchronized void test(){
// todo
}
}
关键词 synchronized加在非静态方法上,这类似与对象级的锁,只不过区别在于加在方法上是将当前整个方法都变成线程安全的,而使用代码块形式的时候是可以灵活的使得某一些代码变成线程安全的
3. 使用jdk提供了基于代码的锁类实现线程安全(ReentratnLock锁)
jdk提供的锁。最普遍的就是java.util.concurrent包下的ReentrantLock类,这是Jdk提供的基于代码层面的锁机制,需要在使用之后手动释放锁,如果不手动释放的话会导致死锁。一般情况就是将释放锁资源的方法放在finally代码块中,确保一定可以执行到。主要利用CAS+AQS队列来实现。
代码:
public class LockTest{
ReentrantLock lock = new ReentrantLock();
// lock.lock()方法
private void test(){
try{
// 加锁,如果获取不到锁就一直等待
lock.lock();
//todo...同步代码
}finally{
//释放锁
lock.unlock();
}
}
// lock.tryLock()方法
private void test(){
try{
// 加锁,如果获取不到锁就立马返回,也可以设置等待时间
lock.tryLock();
//todo...同步代码
}finally{
//释放锁
lock.unlock();
}
}
}
-
其他的同步机制
-
volatile关键词
代码:
public class test{ //使用volatile修饰变量 private volatile boolean isFlag = false; private int i = 0; public void test(){ //循环执行代码 while(!isFlag){ //todo.. if (i++ > 10){ isFlag = true; } } } }
volatile修饰变量,可以实现再多个线程之间的可见性,即线程A改变了值,线程B下次读取的时候可以获得最新的值,原理是编译之后再变量变量前面加LOCK,当值之后,会强制刷新缓存使得各个CUP的缓存都失效,下个线程读取的时候就要去主内存读取最新的值。该变量只能保证一致性和看见性,不能保证原子性,所以一般用作标志量的修饰。
-
ThreadLocal类
代码:
public class test{ //使用ThreadLocal,并使用withInital初始化,以免再set之前使用get出现空指针异常,默认是null ThreadLocal<Integer> local = ThreadLocal.withInitial(() -> 0); public void test1(){ local.set(100); System.out.println(Thread.currentThread().getName() + "输出:" + local.get()) // local.remove();使用完记得清除 } }
线程的本地变量使用也很简单,每个线程都会得到一个local的副本,各个线程之间不会有影响,实例:数据库的session的管理,这样即不需要每一个数据库连接都创建一个新的变量,节省了资源而且session的事务和关闭也不互相影响。底层使用的map,再使用完毕的时候记得清除掉,不会导致gc无法回收出现内存溢出
- CyclicBarrier(循环屏障)
代码:
public class test{ CyclicBarrier cyclic = new CyclicBarrier(2); Runnable runnable = ()->{ System.out.println(Thread.currentThread().getName() + "启动"); try { //等待 cyclic.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "完成"); Thread thread1 = new Thread(runable,"线程1); Thread thread1 = new Thread(runable,"线程2); thread1.start(); thread.start(); }
输出:
线程1启动
线程2启动
线程1完成
线程2完成
两个线程相互等待,知道等待的线程数达到设置的“2”个的时候,同时执行后面的代码。即多个线程相互等待,知道满足设置的条件,再一同执行接下来的代码。
- CountDownLatch(递减锁存器)
-
代码:
public class CountDownLatchTest {
public static void main(String[] args) {
latchLock();
}
private static void latchLock() {
CountDownLatch countDownLatch = new CountDownLatch(4);
//等待线程
Runnable runnable = () -> {
System.out.println("你好!");
try {
//倒计时锁存器等待
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("再见!");
};
//执行线程
Runnable runnable2 = () -> {
//模仿多个线程执行代码,countDownLatch.countDown();
for (int i = 0; i < 4; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
}
};
Thread thread = new Thread(runnable, "等待线程");
Thread thread1 = new Thread(runnable2, "执行线程");
thread1.start();
thread.start();
}
}
一个线程等待其他线程执行完毕,再接着执行接下来的代码,当执行countDown();方法的时候会减1直到不大于0的时候,再去执行方法await()等待的线程。不一定时多个线程,只要设定的值满足条件即可继续唤醒等待的线程继续执行。
-
原子类
使用JUC包的的Atomic开头的,使用的时CAS算法保证线程安全。
总结
- synchronized是JVM层面的同步机制,由JVM托管锁的释放,不会出现锁无法释放的情况。再JDK1.6之前是重量级,从JDK1.6开始进行了优化,加入了锁升级的概论(无锁–>偏向锁–>轻量级锁–>重量级锁)。
- ReentrantLock锁是JDK层面的锁,如果不能手动释放锁会出现死锁的情况,除此之外华友读写锁等。
- 关键词(volatile,ThreadLocal,CyclicBarrier,CountDownLatch)都是同步辅助工具,可随具体业务和场景使用。