一.使用synchronized关键字
由于每个java对象都有一个内置锁,用synchronized修饰方法或者代码块时,内置锁会保护整个方法或代码块,要想执行这个方法或者代码块必须获得其内置锁,运行时会加上内置锁,当运行结束时,内置锁会打开。由于同步是一种高开销的工作,所以尽量减少同步的内容,只需同步代码块就可以。
1.修饰方法
public class Test implements Runnable {
static int i=0;
public synchronized void test(){
System.out.println("当前线程为"+i);
i++;
}
public void run(){
test();
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Test t1=new Test();
for(int i=0;i<10;i++){
Thread t11=new Thread(t1);
t11.start();
}
}
}
结果如下:
从上面结果看出,使用synchroniz修饰方法会在每个线程中按顺序依次执行。
在使用该方法时要注意以下一点
先看代码如下:
public class Test implements Runnable {
static int i=0;
public synchronized void test(){
System.out.println("当前线程为"+i);
i++;
}
public void run(){
test();
}
public static void main(String[] args) {
// TODO Auto-generated method stub
for(int i=0;i<10;i++){
Test t1=new Test();
Thread t11=new Thread(t1);
t11.start();
}
}
}
在看下结果:
只是稍微修改了一处,结果就有很大不同,为什么会这样呢?我们发现上面两个代码,一个是在开始值创建一个Runnable的对象,一个是在for循环中每次都创建一个新的对象,就是因为synchronized是不能锁住不同对象的线程的,只能锁住同一个对象的线程,也就是说锁住的是方法所属的主体对象自身。
2.修饰代码块
public class Test implements Runnable {
static int i=0;
public void test(){
synchronized(this){System.out.println("当前线程为"+i);
i++;
}
}
public void run(){
test();
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Test t1=new Test();
for(int i=0;i<10;i++){
Thread t11=new Thread(t1);
t11.start();
}
}
}
运行结果和第一个程序一样的,不多解释了。
3.修饰静态方法
public static synchronized void anotherMethod() {
// do something
}
对于静态方法,锁住的不是这个类的对象,也不是也不是这个类自身,而是这个类所属的java.lang.Class类型的对象
二.wait与notify
- wait(),使一个线程处于等待状态,并释放所持对象的锁,与sleep不同,sleep不会释放对象锁。
- notify(),唤醒一个处于阻塞状态的线程,进入就绪态,并加锁,只能唤醒一个线程,但不能确切知道唤醒哪一个,由JVM决定,不是按优先级。其实不是对对象锁的唤醒,是告诉调用wait方法的线程可以去竞争对象锁了。wait和notify必须在synchronized代码块中调用。
- notifyAll(),唤醒所有处于阻塞状态的线程,并不是给他们加锁,而是让他们处于竞争。
为什么wait和notify要在synchronized代码块中使用
调用wait()就是释放锁,释放锁的前提是必须要先获得锁,先获得锁才能释放锁,释放锁后进入等待队列。
notify(),notifyAll()是将锁交给含有wait()方法的线程,让其继续执行下去,如果自身没有锁,怎么叫把锁交给其他线程呢;(本质是让处于阻塞队列的线程进入等待队列竞争锁)
Synchronized应用举例:生产者消费者模型
消费者线程需要等待直到生产者线程完成一次写入操作。生产者线程需要等待消费者线程完成一次读取操作。假设没有应用Synchronized关键字,当消费者线程执行wait操作的同时,生产线线程执行notify,生产者线程可能在等待队列中找不到消费者线程。导致消费者线程一直处于阻塞状态。那么这个模型就要失败了。所以必须要加Synchronized关键字。
生产者消费者代码实现:
package p2;
//使用wait与notify实现
public class ProducerConsumer {
public static void main(String[] arg) {
Resource resource = new Resource();
// 生产者线程
ProducerThread p1 = new ProducerThread(resource);
ProducerThread p2 = new ProducerThread(resource);
ProducerThread p3 = new ProducerThread(resource);
// 消费者线程。测试时可以少开几个消费线程看看具体
ConsumerThread c1 = new ConsumerThread(resource);
ConsumerThread c2 = new ConsumerThread(resource);
ConsumerThread c3 = new ConsumerThread(resource);
p1.start();
p2.start();
p3.start();
c1.start();
c2.start();
c3.start();
}
}
// 编写资源类
class Resource {
//当前资源池数量
private int currentSize = 0;
//允许数量
private int allowSize = 10;
// 取走资源,如果当前资源大于0则可以移除(消费),移除之后唤醒生产线程。否则进入等待释放线程资源
public synchronized void remove() {
if (currentSize > 0) {
currentSize--;
System.out.println(Thread.currentThread().getName() + "消费一件资源,当前资源池有" + currentSize + "个");
notifyAll();
} else {
// 没有资源 消费者进入等待状态
try {
System.out.println(Thread.currentThread().getName() + "当前资源过少,等待增加");
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public synchronized void add() {
// 如果当前数量小于限制数量则可以增加,增加后唤醒消费者消费,否则等待消费,释放锁
if (currentSize < allowSize) {
currentSize++;
System.out.println(Thread.currentThread().getName() + "生产一件资源,当前资源池有" + currentSize + "个");
notifyAll();
} else {
try {
System.out.println(Thread.currentThread().getName() + "当前资源过多,等待消费");
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
//消费线程
class ConsumerThread extends Thread {
private Resource resource;
ConsumerThread(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true) {
//避免生产消费太快测试的时候看不到打印,休眠一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//移除代表消费
resource.remove();
}
}
}
//生产者线程
class ProducerThread extends Thread {
private Resource resource;
ProducerThread(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//生产
resource.add();
}
}
}
三.volatile关键字
前面提到的synchronized关键字锁住的是代码块,但是容易造成资源的浪费,是一种重量锁,而volatile是一种轻量锁,锁住的是变量。
我们先来看下面的代码:
boolean value=false;
--------------线程2-------------
public void change(){ value=true; }
--------------线程1--------------
public void output(){ if(value==true) System.out,println("输出成功"); }
上面的结果会输出什么呢?先执行线程2,再执行线程1,是否会输出成功呢?其实是不一定的,让我们来看下为什么:
上面这张图表明,不同线程在执行时,数据都是从主内存中取得的,每个线程自身都有一个工作内存。线程读入和写入数据的过程如下:
先从主内存中读取数据,放入工作内存,传递到线程中使用,在修改数据时,原路返回,经工作内存再写入到主内存中。
这样就会有问题了,当两个线程1先把修改的数据经过工作内存写回到主内存的过程中,线程二读取主内存中的数据了,这样数据就出现了不一致性,我们把这种情况叫做线程之间不可见性。
volatile关键字就是用来解决这种不可见问题的,它是怎么实现的呢?
- 使用volatile修饰的变量在被一个线程修改后,直接将数据写回到主内存,跳过了工作内存
- 使用volatile修饰的变量,在被线程1修改后,线程二中的该变量就被视为无效
- 线程2中数据无效了,在使用的时候就必须重新回主内存中读取该数据
但是在使用volatile时需要注意一点,我们举个例子来说
public class Test implements Runnable {
volatile int value=0;
volatile int count=0;
public void run(){
for(int i=0;i<10000;i++){
value++;
}
count++;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Test test=new Test();
for(int i=0;i<5;i++){
Thread t=new Thread(test);
t.start();
}
while(test.count!=5);
System.out.println(test.value);
}
}
看下结果:
按理来说应该会输出50000,为什么会使那个结果呢?
原因在于volatile不能满足原子性。
原子性是指一个操作要么在执行时不被打断,要么就是不执行,原子操作只有最简单的赋值和读取。我们举例子讲解一下:
int a=1;
int b=a;
a++;
上面三个只有第一个是原子操作,第二个,先读取a的值,再赋值给b,两个原子操作叠加起来就不是原子操作,第三个,相当于a=a+1,先读取a的值,再加1,再赋值给a,也不是原子操作。
注意:long和double的变量是64位的,不满足原子操作。
了解了原子性我们想想为什么之前代码的结果不是500000。因为value的递增不是原子操作,volatile是无法保证原子性的。我们可以假设有这种情况,当value为100时线程1执行value自增的时候,比如说进行到了加1操作后被阻塞了,线程2接着进行value自增,线程2在主存中读取value值时会发现value还是100,那么线程1和线程2执行的结果都是101,相当于两次自增后value确只增加1,这就造成了实际值比500000小。
四.Lock
前面的synchronized加锁,只有加锁和释放所=锁,在JDK5后出现了新的加锁方法,使用Lock,包含比synchronized更多的加锁功能。ReentrantLock类是实现了Lock接口的锁.ReentrantLock类的常用方法:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
两者区别:
- synchronized是java的关键字,是java的内置特性,是基于JVM层面的,而Lock是接口,是基于javaJDK层面的,通过这个接口可以实现同步访问。
- synchronized是不需要手动释放锁的,在代码执行完后,系统会让线程自动释放锁,但是Lock要手动解锁,如果不手动解锁,会出现死锁现象。
public class Test implements Runnable{
Lock lock = new ReentrantLock();
public void run(){
lock.lock();
try{
String name=Thread.currentThread().getName();
for(int i=0 ; i<5 ;i++){
System.out.println("线程"+name+":"+i);
} }
catch(Exception e){
}
finally{ lock.unlock(); }
}
public static void main(String[] args){
Test test = new Test();
Thread thread1 = new Thread(test,"1");
thread1.start();
Thread thread2 = new Thread(test,"2");
thread2.start(); } }
结果如下:
可以看出线程按顺序执行了,通常将要锁住的代码和方法放在try-catch中,在finally中释放锁,和synchronized一样,是对同一对象的两个线程。
五.ThreadLocal类
使用ThreadLocal管理变量,每一个使用该变量的线程都获得该变量的副本,各个副本之间相互独立,每个线程都可以随意修改变量副本,而不会对其他线程造成影响。
ThreadLocal类的常用方法
ThreadLocal() : 创建一个线程本地变量
get() : 返回此线程局部变量的当前线程副本中的值
initialValue() : 返回此线程局部变量的当前线程的"初始值"
set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
来看一下如何使用:
public class Test implements Runnable{
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
protected Integer initialValue() {
return 0;
}
};
public void run(){
for(int i=0 ; i<5 ;i++)
value.set(value.get()+1);
String name = Thread.currentThread().getName();
System.out.println("线程"+name+":"+value.get());
}
public static void main(String[] args){
Test test = new Test();
Thread thread1 = new Thread(test,"1");
thread1.start();
Thread thread2 = new Thread(test,"2");
thread2.start(); } }
结果如下:
注:ThreadLocal与同步机制
a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
b.前者采用以"空间换时间"的方法,后者采用以"时间换空间"的方式