浅谈Java并发编程(一)
作为一个工作经验还不是很足的初级程序员,第一次写博客,不足之处还希望业内前辈多多指点。以前接触并发编程的机会还是挺少的,但是进入了一个大平台,用户量很可观,所以在开发中并发的情况很会很常见,所以觉得好好了解这方面的知识很有必要,言归正传,接下来我将从一下几点浅谈Java并发编程:
- 为什么使用Java并发编程与多线程的介绍
- 线程安全性的几种情况和解决办法
- 小结
一.为什么使用Java并发编程与多线程的介绍
我们知道在早期,计算机资源是昂贵且稀有的。为了最大的提高资源的利用率,保证不同的用户和程序对于计算机上的资源有着同等的使用权,并且执行不同任务使用不同的程序可以更加便捷,促使进程和线程的出现,线程允许在同一个进程中同时存在多个程序控制流,线程会共享进程范围内的资源,例如内存句柄和文件句柄,但是每个线程都有各自的程序计数器,栈以及局部变量等。由于同一个进程中的线程都将共享进程的内存地址空间,因此这些线程都能访问相同的变量并在同一个堆上分配对象,这就需要实现一种比在进程共享数据粒度更细的数据共享机制。此外多线程中的操作执行顺序是不可预测的,多个线程之间的交替操作以及指令重排序,在最糟糕的情况下将会存在各种可能的风险。除了安全问题,还有活跃性问题(比如无限等待,无限循环)和性能问题(线程调度时,频繁出现上下文切换操作,共享数据必须使用同步机制而抑制某些编译器优化,使内存缓存数据无效,以及增加共享内存总线的同步流,都将导致性能的额外开销)。因此为了开发健壮的程序,必须要熟悉并发性和线程安全性。
二.线程安全性的几种情况和解决办法
1.竟态条件的发生和解决
当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竟态条件。最常见的竟态条件类型就是“先检查后执行”操作(比如延迟初始化,递增操作)为了保证一组操作必须以原子方式执行,从而保证线程安全性。
(1)对于有些情况下只需要添加一个状态变量就可以通过线程安全的对象来管理类的状态,如使用java.util.concurrent.atomic包中包含的一些原子变量类,用于实现在数值上和对象引用上的原子状态转换。
import java.util.concurrent.atomic.AtomicLong;
/**
* * @author : huan.yang
*
* @date 创建时间:2017年5月2日 下午9:20:40
**/
public class AtomicCounterTest {
//非原子操作
private static long counter = 0;
public static long addOne() {
return ++counter;
}
//原子操作
private static AtomicLong atomicCounter = new AtomicLong(0);
public static long atomicAddOne() {
return atomicCounter.incrementAndGet();
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
Thread thread = new Thread() {
@Override
public void run() {
try {
Thread.sleep(100);
if (AtomicCounterTest.addOne() == 100) {
System.out.println("addOne counter = 100");
}
if (AtomicCounterTest.atomicAddOne() == 100) {
System.out.println("atomicAddOne counter = 100");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread.start();
}
}
}
(2)当在不变性条件中涉及多个变量时,各变量之间并不是彼此独立时,某个变量的值会对其他变量的值产生约束时,此时,当更新一个变量时,也需要在同一个原子操作中对其他变量进行同时更新。Java提供了一种内置的锁机制来支持原子性:同步代码块。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。(synchronized修饰方法,同一刻只有一个线程可以执行这个方法,虽然是线程安全的,但是可能会造成一些性能问题,怎么优化,后续学习了再讲)。内置锁是可重入的,比如B的父累是A,B重写了A的doSomething方法,而且都是synchronized修饰的,这时子类可以调用super.doSomething(),虽然调用每个doSomething方法都会获取A上的锁,因为内置锁是可重入的,从而避免了死锁。
package thread.learn1;
/** * @author : huan.yang
* @date 创建时间:2017年5月2日 下午10:22:47
* **/
public class Thread1 implements Runnable{
public void run() {
synchronized(this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " synchronized loop " + i);
}
}
}
public static void main(String[] args) {
Thread1 t1 = new Thread1();
Thread ta = new Thread(t1, "A");
Thread tb = new Thread(t1, "B");
ta.start();
tb.start();
}
}
(3)Lock与ReentrantLock,与内置锁不同的是,Lock提供了一种无条件,可轮询的,定时的以及可中短的锁获取操作,所有枷锁和解锁的方法都是显示的。ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。可定时的与可轮询的锁可避免死锁的发生,使用Lock锁,必须在finally块中释放锁。常用场景有:
i).如果发现该操作已经在执行中则不再执行(有状态执行)
ii).如果发现该操作已经在执行,等待一个一个执行(同步执行,类似synchronized)
iii).如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行(尝试等待执行)
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) { //如果已经被lock,尝试等待5s,看是否可以获得锁,如果5s后仍然无法获得锁则返回false继续执行
try {
//操作
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace(); //当前线程被中断时(interrupt),会抛InterruptedException
}
iv).可中断锁的同步执行
(4)Java.util.concurrent中的同步器之信号量机制 Semaphore,Semaphore维护了当前访问的个数,提供同步机制,控制同时访问的个数,超过限制排队等待。
public class SemaphoreTest {
private static final Semaphore semaphore = new Semaphore(4);
public static void main(String[] args) {
System.out.println(Runtime.getRuntime().availableProcessors());
for(int i=0; i<8; i++){
new Thread(new Runnable(){
@Override
public void run() {
try {
//从定义的semaphore中获取一个执行许可,如果没有可执行许可,会一直被阻塞直到线程中断,或者另一个线程发出释放信号,并且这个线程是下一个可获取许可的。
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " acquired.");
Thread.sleep((long)(Math.random() * 2000));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();//记得释放
}
}
}).start();
}
}
}
三.小结
本篇文章介绍了最基础的并发编程知识,在后续将会深入讨论同步容器,并发容器,以及阻塞队列。文中内容主要是通过看书和网上查找资料,实践总结而来,后续会多加改进,多加自己思考实践。