首先要记住核心一点、多线程事异步的,也就是cpu利用率大幅提高。
Stringbuffer 是线程安全的 stringbuilder是线程不安全的
HashTable是线程安全的 HashMap不是线程安全的
2.对象及变量的并发访问下的问题。
方法内的变量因为是方法的私有变量,所有不存在线程安全的问题。因此方法内的变量是线程安全的。
多个线程如果同时访问一个对象中的实例变量,则该实例变量不是线程安全的。
synchronized
synchronized取得的锁都是对象锁,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。
但如果多个线程访问多个对象,则jvm会创建多个锁。
A线程先持有object对象的lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法。
A线程先持有object对象的lock锁,B线程如果这时调用object对象中的synchronized类型的方法则需等待,也就是同步。
synchronized 拥有锁重入的功能。
锁重入:自己可以再次获取自己的内部锁,例如一条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。
可重入锁也支持在父子类继承的环境中。当存在父子类存在继承关系时,子类是完全可通过可重入锁调用父类的同步方法。
当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
同步是不可以被继承的。
例如父类synchronized关键字修饰过的方法,子类下的该方法是不具备该关键字的。除非子类也自己修饰。
关键字synchronized修饰方法的弊端。
比如A线程调用同步方法执行一个长时间的任务,那么B线程则必须等待比较长时间。这种情况下可以使用synchronized同步语句块来解决问题。
synchronized方法是对当前对象进行加锁,而synchronized代码块是对某一个对象进行加锁。
当一个线程访问object的一个synchronized 同步代码块时,另一个线程仍然可以访问该object对象中的非synchronized(this)同步代码块。
当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问即将被阻塞。这说明synchronized使用的对象监视器是一个。
如果一个类中有很多个synchronized方法,这时虽然能实现同步,但会收到阻塞,所以影响运行效率,但如果使用同步代码块锁非this对象,则synchronized(非this)代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,则可大大提高运行效率。
java还支持对“任意对象”作为“对象监视器”来实现同步的功能。这个“任意对象”大多数时实例变量及方法的参数,使用格式为synchronized(非this对象)
synchronized(非this对象x)是将x对象本身作为“对象监视器”,因此:
1.当多个线程同时执行synchronized(x){}同步代码块时呈同步效果。
2.当其他线程执行x对象中synchronized同步方法时呈同步效果。
3.当其他线程执行x对象方法里面的synchronized(this)代码块时也呈现同步效果。
但需要注意:如果其他线程调用不加synchronized关键字的方法时,还是异步调用。
静态同步synchronized方法与synchronized(class)代码块
关键字synchronized还恶意家用再static静态方法上,这是对当前*.java文件对应的class类进行加锁。
在持有不同的锁的情况下,一个是对象锁,另外一个是class锁,会导致异步运行。class锁会对该类的所有对象实例起作用。
同步synchronized(class)代码块的作用和synchronized static 方法的作用一样。
这里要注意一个数据类型String的常量池特性
将synchronized(string)同步块与String 联合使用时,要注意常量池带来的一些例外。
例子如下:
public class Main { public static void main(String[] args) { Service service =new Service(); ThreadA a=new ThreadA(service); a.setName("A"); a.start(); ThreadB b=new ThreadB(service); b.setName("B"); b.start(); } }
public class Service { public static void print(String param) { try { synchronized (param) { while(true) { System.out.println(Thread.currentThread().getName()); Thread.sleep(500); } } }catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
public class ThreadA extends Thread{ private Service service; public ThreadA(Service service) { super(); this.service=service; } public void run() { service.print("AA"); } }
public class ThreadB extends Thread { private Service service; public ThreadB(Service service) { super(); this.service=service; } public void run() { service.print("AA"); } }
运行结果就是无限打印A A A A A A
出现这样的情况就是因为String 的两个值都是AA,两个线程持有相同的锁,所以导致线程B并不能运行,这就是常量池带来的问题。
因此大多数情况,同步synchronized代码块都不使用String作为锁对象,而用其他,比如 new Object()实例化一个Object对象。
同步方法还有个弊端就是容易造成死循环。假如在某类中有多个synchronized修饰的方法,线程a和b分别访问不同的synchronized修饰过的方法,当a线程访问的方法中出现了死循环,b线程则无法访问另外一个方法,因为当前的对象锁并没有释放。
死锁的问题:当设计程序时,双方互相持有了对方的锁,就会造成死锁。原因就是线程互相等待对方释放锁。
以上的synchronized 方法和方法块同样适用于内部类和静态内部类。
要注意一种情况:就是在线程运行中锁对象发生了改变。
例如当前有个 String lock="123"的字符串,当synchronized(lock)的时候,如果在该同步代码块中,lock的值发生了改变,例如变成了456.那么同时访问改代码块的另外一个线程即可立即访问,这就是锁对象发生了改变。
还有一种情况就是synchronized(user)注:user 是User的对象,那么在某线程执行该代码块的时候,改变了user中某一属性的值,例如user.setUsername("111"),则另一访问该代码块的线程并不会立即获得该代码块的锁对象,因此原则就是只要对象不变,即使对象的某个属性发生变化,运行的结果还是同步。
volatile
volatile关键字的主要作用是使变量在多个线程间可见。
关键字volatile的作用是强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。
图2-75代表的是没有用volatile关键字的读取某变量的模式
通过使用volatile关键字,强制从公共内存中读区变量的值
volatile最致命的缺点是不支持原子性
volatile与synchronized的比较:
1.volatile是线程同步的轻量级实现,所以性能要比synchronized要好,并且volatile只能修饰变量,而synchronized可以修饰方法,以及代码块。
2.多线程访问volatile不会发生阻塞,而synchronized会出现阻塞
3.volatile能保证数据的可见性,但不能保证原子性。而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公公内存中的数据做同步。
4.最后终点重申,volatile解决的是变量在多个线程之间的可见性,而synchronized解决的是多个线程之间访问资源的同步性。
此处提一下线程安全:线程安全包含原子性和可见性,java的同步机制都是围绕这两个方面来确保线程安全的。
解释一下volatile非原子的特性:
如果修改实力变量中的数据,比如i++,这样一个操作并不是原子操作。也就是非线程安全的。i++的操作步骤分解如下:
1)从内存中取出i的值
2)计算i的值
3)将i的值写到内存中。
假如第二部计算i的值的时候,另外一个线程也修改i的值,此时就会出现脏数据。解决的办法其实就是使用synchronized关键字。
下图演示一下volatile时出现非线程安全的原因。
use和assign时多次出现,但这一操作并不是原子性,也就是在read和load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应变化,也就是私有内存和公共内存中的变量不同步,所以计算出来的结果和预期不一样,就会出现非线程安全的问题。
综上所述,volatile关键字解决的是变量读时的可见性问题,但无法保证原子性,对于多个线程访问同一个实例变量还是需要加锁同步。
synchronized代码块其实也有volatile同步的功能。
synchronized不仅可以使多个线程访问同一个资源具有同步性,而且它还有句将线程工作内存中的私有变量和与公共内存中的变量同步的功能。
关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法或代码块,它包含两个特征:互斥性和可见性。同步synchronized不仅可以解决一个线程看到对象不一致的状态,(比如,修改了某个对象后,另外一个线程即可获得修改后的对象的锁。)还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护之前所有的修改效果。
3.线程间通信的详解
使线程间进行通信后,系统之间的交互性会更加强大,在大大提高CPU利用率的同时还会使程序员对个线程任务在处理的过程中进行有效的把控与监督。
等待/通知机制我们通过wait/notify方法来实现。
要注意的是:多个线程共同访问一个变量,也是一种通信,但那种通信机制不是“等待/通知”,两个线程完全是主动式地读取一个共享变量,在花费读取时间的基础上,读到的值是不是想要的,并不能完全确定。
在调用wait()方法之前,线程必须获得该对象的对象级别锁,即只能在同步方法或者同步代码块中调用wait()方法。在执行wait方法后,当前线程释放锁。
方法notify()同样要在同步方法或同步块中调用,即在调用前,线程也必须获得该对象的对象级别锁。
要注意的是:在执行notify方法后,当前线程不会立即释放该对象锁,呈wait状态的线程也不能马上获取该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才会释放锁,而呈wait状态所在的线程才可以获取该对象锁。当获得了该对象锁的线程运行完毕后,它会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即使该对象已经空闲,其他wait状态的线程由于没有得到该对象的通知,还会继续阻塞在wait状态,直到这个对象发出一个notify()或者notifyAll().
一句话总结wait和notify 就是wait使线程停止运行,notify使停止的线程继续运行。
notify方法可以随机唤醒等待队列中等待同一共享资源的“一个”线程,并使该线程退出等待队列,进入可运行状态。
notifyAll()方法使所有在等待队列中等待同一共享资源的“全部”线程从等待状态退出,进入可运行状态,此时优先级最高的那个线程最先执行,但也有可能是随机执行,这要取决于JVM虚拟机的实现。
每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程。一个线程被唤醒后,才会进入就绪队列,等待cpu的调度,反之,一个线程被wait后,就会进入阻塞队列,等待下一次被唤醒。
方法wait(long)带一个参数的方法功能室等待某一时间内是否有线程对锁紧型唤醒,如果超过这个时间则自动唤醒。
还要注意notify的时候,有个通知过早的问题,不要在另外一个线程wait之前就notify 这样会导致wait的线程永远也得不到通知。
方法join的使用:
很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程往往将早于子线程结束之前结束。这是如果主线程响等待子线程执行完成之后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到join()方法了。
方法join的作用是等待线程对象销毁。
详细来说,是使所属的线程对象x正常执行run()方法中的任务,而使当前线程z进行无限期的阻塞,等待线程x销毁后再继续执行线程z后面的代码。
join具有使线程排队运行的作用,有些类似同步的运行效果,join与synchronized的区别是:join在内部使用wait()方法进行等待,而synchronized关键字使用的是“对象监视器”原理作为同步。
join也有join(long)的方法,是设定等待的时间。join(long)内部是使用wait(long)的方法实现的,所以有释放锁的特点。
而Thread.sleep(long)方法却不释放锁。
类ThreadLocal的使用
类ThreadLocal主要解决的是每个线程绑定自己的值。
类Threadlocal解决的是变量在不同线程间的隔离性,也就是不同线程拥有自己的值,不同线程中的值是可以放入Threadlocal类中进行保存的。
在不给ThreadLocal类中的静态变量使用set方法之前,用get方法返回的都是null。
可以通过创建一个子类继承ThreadLocal类,里面有一个方法initialValue()来设定初始值。
public class ThreadLocalExt extends ThreadLocal {
protected Object initialValue() {
return "我是默认值,第一次get不再为null";
}
}
使用InheritableThreadLocal类可以让子线程从父线程中取得值。
public class InheritableThreadLocalExt extends InheritableThreadLocal {
protected Object initialValue() {
return new Date().getTime();
}
}
通过继承InheritableThreadLocal类,可以使父子线程通过 public static InheritableThreadLocalExt tl=new InheritableThreadLocalExt();得到的tl的值是一致的。
如果想修改子线程的值,可以重写以下方法
protected Object childValue(Object parentValue) {
return parentValue+"我在子线程加的";
}
使用该InheritableThreadLocal类需要注意的一点就是,如果子线程在取得值的同时,主线程将InheritableThreadLocal中的值进行更改,那么子线程取到的值还是旧值。
Lock的使用
ReentranLock也可以实现等待/通知模式,利用Condition丢翔。Condition可以实现多路通知功能。也就是在一个Lock对象里面可以创建多个Condition实例,线程对象可以注册在指定的Condition中,从而可以有选择的进行线程通知,在调度线程上更加灵活。
在使用notify/notifyall方法进行通知时,被通知的线程却是由JVM随机选择的。但使用ReentrantLock结合Condition类是可以实现前面介绍过的选择性通知,这个功能很重要。
Object类中的wait()方法相当于Condition类中的await()方法.
Object类中的wait(long)方法相当于Condition类中的await(long time, TimeUnit unit)方法。
Object类中的notify()方法相当于Condition类中的signal()方法。
Object类中的notifyAll()方法相当于Condition类中的signalAll()方法。
注意:在condition.await()调用之前 要先调用lock.lock() 获得监视器。
如果想单独唤醒部分线程就要使用多个Condition对象了,也就是condition对象可以唤醒部分指定线程。
锁Lock分为公平锁与非公平锁。
公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,既先来先得的FIFO先进先出顺序,而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一致拿不到锁,结果也就是不公平的了。
方法int getHoldCount()的作用是查询当前线程保持此锁定的个数,也就是调用lock()方法的次数。
方法 intgetQueueLength()的作用是反悔正等待此锁定的线程估计数。