最主要还是压榨硬件(上图为我cpu的使用率)。现在硬件都是过剩的状态,不压榨干嘛,也不能天天指望程序员拿头发来优化算法啊。
都知道并发编程能提高效率,但是这肯定有代价的,会带来很多问题主要分为3大类。
原子性:一个或多个操作在cpu执行过程中不可分割。不可分割也就是中间状态对外不可见,所以只要保证对外不可见即可。
为什么会有这个问题?
cpu会类似上图每隔一定时间铁环线程执行从而达到,多个程序同时在执行的感觉。也就是说你的程序执行到一半,就可能切换走了。java是种高级语言,一行往往对应多条cpu指令,加上cpu指令冲排序,很可能结果线出来,但是过程还没完成,一旦被外界拿去用,就会出现问题。比如Object o=new Object();
这一部操作对应了以下3步。
如果这个过程对外不可见,随便怎么重拍都无所谓,但是如果2和3掉了个位置,别人用的时候发现没有初始化,很可能就会出现问题。这还只是一行命令,就已经出现了风险,多行语句出问题的概率更大
如何解决?
一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。并发程序一旦死锁,一般没有特别好的方法,很多时候我们只能重启应用。因此,解决死锁问题最好的办法还是规避死锁。
有时线程虽然没有发生阻塞,但仍然会存在执行不下去的情况,这就是所谓的“活锁”
所谓“饥饿”指的是线程因无法访问所需资源而无法执行下去的情况。优先级低的线程得到执行的机会很小,就可能发生线程“饥饿”;持有锁的线程,如果执行的时间过长,也可能导致“饥饿”问题。
不能适当的发挥多线程性能的优势,不能为了用多线程而用多线程,因为用锁会不可避免带来一点性能问题,很可能在某些情况下很可能执行时间还不如多线程。我们之所以使用多线程搞并发程序,为的就是提升性能。
java多线程基本上都是基于Monitor实现的
管程博客这个博客写的不错建议看这个
synchronized首先说下如何使用,作用于方法,作用于同步代码块。当写在静态方法中,锁的是Class对象,和同步代码块中写(xxx.class)效果一致,下面有个测试代码,感兴趣的可以测试下
public class SynchronizedDemo {
public synchronized void test1() throws InterruptedException {
System.out.println("test1");
Thread.sleep(10000);
System.out.println("test1 end");
test3();
}
public static synchronized void test2() throws InterruptedException {
System.out.println("test2");
Thread.sleep(10000);
System.out.println("test2 end");
}
public void test3() throws InterruptedException {
synchronized (this){
System.out.println("test3");
Thread.sleep(10000);
System.out.println("test3 end");
}
}
public void test4() throws InterruptedException {
synchronized (SynchronizedDemo.class){
System.out.println("test4");
Thread.sleep(10000);
System.out.println("test4 end");
test2();
}
}
public static void main(String[] args) {
SynchronizedDemo synchronizedDemo=new SynchronizedDemo();
Runnable r=new Runnable() {
@Override
public void run() {
try {
// synchronizedDemo.test1();
synchronizedDemo.test2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable r1=new Runnable() {
@Override
public void run() {
try {
// synchronizedDemo.test3();
synchronizedDemo.test4();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t1=new Thread(r);
Thread t2=new Thread(r1);
// t1.start();
t2.start();
}
}
synchronized用的锁其实是存在java对象头中,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明如下表:
虚拟机位数 | 头对象结构 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashCode、锁信息或分代年龄或GC标志等信息 |
32/64bit | Class Metadata | Address 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。 |
Mark Word在不同的锁状态下存储的内容不同
当只有一个线程获取锁时,偏向锁的标识改成1,线程id,时间戳。当有其他线程尝试获取锁的时候,会判断当前线程id和markword里面的线程id是否是一致,不一致,膨胀为轻量级锁,然后自旋比较,还没有拿到锁,就膨胀为重量级锁,重量级锁的指针就指向了一个monitor对象,结构如下
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
当多个线程同时访问一段同步代码时,首先会进入_EntryList队列中,当某个线程获取到对象的monitor后进入_Owner区域并把monitor中的_owner变量设置为当前线程,同时monitor中的计数器_count加1。即获得对象锁。
若持有monitor的线程调用wait()方法,将释放当前持有的monitor,_owner变量恢复为null,_count自减1,同时该线程进入_WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示
该关键字确保了对一个变量的更新对其他线程可见。当一个变量被声明为volatile时候,线程写入时候不会把值缓存在寄存器或者或者在其他地方,当线程读取的时候会从主内存重新获取最新值,而不是使用当前线程的拷贝内存变量值。volatile虽然提供了可见性保证,但是不能使用他来构建复合的原子性操作,也就是说当一个变量依赖其他变量或者更新变量值时候新值依赖当前老值时候不在适用。
如图线程A修改了volatile变量b的值,然后线程B读取了改变量值,那么所有A线程在写入变量b值前可见的变量值,在B读取volatile变量b后对线程B都是可见的,图中线程B对A操作的变量a,b的值都可见的。volatile的内存语义和synchronized有类似之处,具体说是说当线程写入了volatile变量值就等价于线程退出synchronized同步块(会把写入到本地内存的变量值同步到主内存),读取volatile变量值就相当于进入同步块(会先清空本地内存变量值,从主内存获取最新值)。转自
final基础用法想必大家都已经了解了,他在多线程中的比较重要的2个点。
参考书籍
《实战Java高并发程序设计》