在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使用线程同步。
基本用法:
synchronized (非匿名的任意对象 obj) {
线程要操作的共享数据
}
以上代码的含义是:obj对象有一把锁,如果当前线程获得了obj对象的锁,那么该线程就有权限执行同步代码块里的代码。一旦执行完同步代码后,该线程也会主动释放对象的锁。如果该线程在执行该同步代码块的过程中CPU时间片用完,进行线程切换,由于同步代码块里的代码未全部执行完毕,所有该线程也不会释放对象的锁,那么其它线程即便获得CPU的使用权利,因为没有obj对象的锁,也无法执行此同步代码块,这样就避免了多线程带来的数据被互斥访问和覆盖的问题,也就实现了线程同步。
我们可以通过线程的状态转换图,更容易理解上面的概念。

实例: 妈妈有两个孩子,分别是大林和小林。妈妈买了50根冰棍放进冰箱。大林和小林每天放学回家都吃冰棍。使用java多线程来描述上述故事并统计大林和小林各吃了多少根冰棍。
//儿子接口类
class Son implements Runnable {
private IceBox iceBox; //冰箱对象
private int bigSonNum = 0; //大儿子吃的数量
private int smallSonNum = 0; //小儿子吃的数量
public Son(IceBox iceBox) {
this.iceBox = iceBox;
}
@Override
public void run() {
//不停的吃冰棍
while (true) {
//谁有iceBox这个对象的锁,谁就有资格进入同步代码块,执行里面的代码。同步代码块执行完后,也会主动交还对象的锁。
synchronized (iceBox) {
if (IceBox.iceScreamNumber <= 0) { //冰棍吃完了。。。。
break; //退出循环,也就意味着线程结束...
}
IceBox.iceScreamNumber--;
if ("大林".equals(Thread.currentThread().getName())) {
System.out.println("大林吃了一根冰棍,还剩:"+IceBox.iceScreamNumber+"根冰棍。");
bigSonNum++;
} else {
System.out.println("小林吃了一根冰棍,还剩:"+IceBox.iceScreamNumber+"根冰棍。");
smallSonNum++;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void showResult() {
System.out.println("大林吃了:" + bigSonNum + "根。");
System.out.println("小林吃了:" + smallSonNum + "根。");
}
}
//冰箱类
public class IceBox {
public static int iceScreamNumber = 50; //冰箱里面的50根冰棍
public static void main(String[] args) {
IceBox iceBox = new IceBox(); //冰箱对象。
Son son = new Son(iceBox); //Runnable对象
Thread bigTh = new Thread(son, "大林"); //大儿子线程对象
Thread smallTh = new Thread(son, "小林"); //小儿子线程对象。
bigTh.start(); //启动大儿子线程
smallTh.start();//启动小儿子线程
try {
bigTh.join(); //等待两个线程结束
smallTh.join();
} catch (Exception ex) {
ex.printStackTrace();
}
son.showResult(); // 显示结果
}
}
运行结果:
大林吃了一根冰棍,还剩:49根冰棍。
大林吃了一根冰棍,还剩:48根冰棍。
大林吃了一根冰棍,还剩:47根冰棍。
大林吃了一根冰棍,还剩:46根冰棍。
大林吃了一根冰棍,还剩:45根冰棍。
大林吃了一根冰棍,还剩:44根冰棍。
大林吃了一根冰棍,还剩:43根冰棍。
大林吃了一根冰棍,还剩:42根冰棍。
大林吃了一根冰棍,还剩:41根冰棍。
大林吃了一根冰棍,还剩:40根冰棍。
大林吃了一根冰棍,还剩:39根冰棍。
大林吃了一根冰棍,还剩:38根冰棍。
大林吃了一根冰棍,还剩:37根冰棍。
大林吃了一根冰棍,还剩:36根冰棍。
大林吃了一根冰棍,还剩:35根冰棍。
大林吃了一根冰棍,还剩:34根冰棍。
大林吃了一根冰棍,还剩:33根冰棍。
大林吃了一根冰棍,还剩:32根冰棍。
大林吃了一根冰棍,还剩:31根冰棍。
大林吃了一根冰棍,还剩:30根冰棍。
大林吃了一根冰棍,还剩:29根冰棍。
大林吃了一根冰棍,还剩:28根冰棍。
大林吃了一根冰棍,还剩:27根冰棍。
大林吃了一根冰棍,还剩:26根冰棍。
大林吃了一根冰棍,还剩:25根冰棍。
大林吃了一根冰棍,还剩:24根冰棍。
大林吃了一根冰棍,还剩:23根冰棍。
小林吃了一根冰棍,还剩:22根冰棍。
小林吃了一根冰棍,还剩:21根冰棍。
小林吃了一根冰棍,还剩:20根冰棍。
小林吃了一根冰棍,还剩:19根冰棍。
小林吃了一根冰棍,还剩:18根冰棍。
小林吃了一根冰棍,还剩:17根冰棍。
小林吃了一根冰棍,还剩:16根冰棍。
小林吃了一根冰棍,还剩:15根冰棍。
小林吃了一根冰棍,还剩:14根冰棍。
小林吃了一根冰棍,还剩:13根冰棍。
小林吃了一根冰棍,还剩:12根冰棍。
小林吃了一根冰棍,还剩:11根冰棍。
小林吃了一根冰棍,还剩:10根冰棍。
小林吃了一根冰棍,还剩:9根冰棍。
小林吃了一根冰棍,还剩:8根冰棍。
小林吃了一根冰棍,还剩:7根冰棍。
小林吃了一根冰棍,还剩:6根冰棍。
小林吃了一根冰棍,还剩:5根冰棍。
小林吃了一根冰棍,还剩:4根冰棍。
小林吃了一根冰棍,还剩:3根冰棍。
小林吃了一根冰棍,还剩:2根冰棍。
小林吃了一根冰棍,还剩:1根冰棍。
小林吃了一根冰棍,还剩:0根冰棍。
大林吃了:27根。
小林吃了:23根。
基本用法:
void synchronized shareFunction(){
//线程要操作的共享数据
}
以上代码的含义是:obj对象有一把锁,如果当前线程获得了调用当前方法的哪个对象的锁,那么当前线程就有权限执行此对象的同步代码块。一旦执行完同步方法,该线程也会主动释放对象的锁。
我们把大林小林吃冰棍的问题,改写为使用同步方法实现代码如下:
//儿子接口类
class Son implements Runnable {
private IceBox iceBox; //冰箱对象
public Son(IceBox iceBox) {
this.iceBox = iceBox;
}
@Override
public void run() {
//不停的吃冰棍
while (true) {
if(IceBox.iceScreamNumber<=0){
break;
}
iceBox.eatIceScream(); //所在线程调用同步方法
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//冰箱类
public class IceBox {
public static int iceScreamNumber = 50; //冰箱里面的50根冰棍
private int bigSonNum = 0; //大儿子吃的数量
private int smallSonNum = 0; //小儿子吃的数量
//定义冰箱对象的同步方法
public synchronized void eatIceScream(){
IceBox.iceScreamNumber--;
if ("大林".equals(Thread.currentThread().getName())) {
System.out.println("大林吃了一根冰棍,还剩:" + IceBox.iceScreamNumber + "根冰棍。");
bigSonNum++;
} else {
System.out.println("小林吃了一根冰棍,还剩:" + IceBox.iceScreamNumber + "根冰棍。");
smallSonNum++;
}
}
public void showResult() {
System.out.println("大林吃了:" + bigSonNum + "根。");
System.out.println("小林吃了:" + smallSonNum + "根。");
}
public static void main(String[] args) {
IceBox iceBox = new IceBox(); //冰箱对象。
Son son = new Son(iceBox); //Runnable对象
Thread bigTh = new Thread(son, "大林"); //大儿子线程对象
Thread smallTh = new Thread(son, "小林"); //小儿子线程对象。
bigTh.start(); //启动大儿子线程
smallTh.start();//启动小儿子线程
try {
bigTh.join(); //等待两个线程结束
smallTh.join();
} catch (Exception ex) {
ex.printStackTrace();
}
iceBox.showResult(); // 显示结果
}
}
执行结果:
大林吃了一根冰棍,还剩:49根冰棍。
小林吃了一根冰棍,还剩:48根冰棍。
小林吃了一根冰棍,还剩:47根冰棍。
大林吃了一根冰棍,还剩:46根冰棍。
大林吃了一根冰棍,还剩:45根冰棍。
小林吃了一根冰棍,还剩:44根冰棍。
大林吃了一根冰棍,还剩:43根冰棍。
小林吃了一根冰棍,还剩:42根冰棍。
大林吃了一根冰棍,还剩:41根冰棍。
小林吃了一根冰棍,还剩:40根冰棍。
小林吃了一根冰棍,还剩:39根冰棍。
大林吃了一根冰棍,还剩:38根冰棍。
大林吃了一根冰棍,还剩:37根冰棍。
小林吃了一根冰棍,还剩:36根冰棍。
小林吃了一根冰棍,还剩:35根冰棍。
大林吃了一根冰棍,还剩:34根冰棍。
小林吃了一根冰棍,还剩:33根冰棍。
大林吃了一根冰棍,还剩:32根冰棍。
小林吃了一根冰棍,还剩:31根冰棍。
大林吃了一根冰棍,还剩:30根冰棍。
小林吃了一根冰棍,还剩:29根冰棍。
大林吃了一根冰棍,还剩:28根冰棍。
小林吃了一根冰棍,还剩:27根冰棍。
大林吃了一根冰棍,还剩:26根冰棍。
小林吃了一根冰棍,还剩:25根冰棍。
大林吃了一根冰棍,还剩:24根冰棍。
小林吃了一根冰棍,还剩:23根冰棍。
大林吃了一根冰棍,还剩:22根冰棍。
小林吃了一根冰棍,还剩:21根冰棍。
大林吃了一根冰棍,还剩:20根冰棍。
大林吃了一根冰棍,还剩:19根冰棍。
小林吃了一根冰棍,还剩:18根冰棍。
小林吃了一根冰棍,还剩:17根冰棍。
大林吃了一根冰棍,还剩:16根冰棍。
小林吃了一根冰棍,还剩:15根冰棍。
大林吃了一根冰棍,还剩:14根冰棍。
小林吃了一根冰棍,还剩:13根冰棍。
大林吃了一根冰棍,还剩:12根冰棍。
小林吃了一根冰棍,还剩:11根冰棍。
大林吃了一根冰棍,还剩:10根冰棍。
大林吃了一根冰棍,还剩:9根冰棍。
小林吃了一根冰棍,还剩:8根冰棍。
小林吃了一根冰棍,还剩:7根冰棍。
大林吃了一根冰棍,还剩:6根冰棍。
小林吃了一根冰棍,还剩:5根冰棍。
大林吃了一根冰棍,还剩:4根冰棍。
大林吃了一根冰棍,还剩:3根冰棍。
小林吃了一根冰棍,还剩:2根冰棍。
小林吃了一根冰棍,还剩:1根冰棍。
大林吃了一根冰棍,还剩:0根冰棍。
大林吃了:25根。
小林吃了:25根。
从JDK1.5开始官方推荐使用Lock接口替代synchronized关键字。
使用synchronized有以下的缺陷: 如果获取锁的线程由于要等待IO或者其它原因(比如sleep)被阻塞了,但是又没有释放锁,其它线程便只能等待,这样非常影响程序执行的效率。因此就需要一种机制:可以不让线程一直无期限等待下去(比如只等待一定时间或者能够相应中断),通过Lock就可以办到。
又假设当多个线程读写文件时,read-write会发生冲突现象,write-write会发生冲突,但是read-read不会发生冲突。如果采用synchronized,就不能让read-read同时进行,只要有一个线程read,其他想read的线程都只能等待,严重影响效率。一次需要一种机制:使得多个线程都只是read时,线程之间不会发生冲突,通过Lock就可以办到。
另外通过Lock可以知道线程有没有成功获取到锁,这个是synchronized无法办到的。
Lock和synchronized的区别:
1).Lock是一个接口,不是Java语言内置的,synchronized是java语言内置的关键字。
2).Lock与synchronized有一点非常大的不同,采用synchronized不需要用户区手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户区手动释放锁,如果没有主动释放锁,就有可能导致出现死锁。
ReenreantLock是Lock接口的实现类,ReenreantLock类的常用方法有:
1).ReentrantLock() : 创建一个ReentrantLock实例 2).lock() : 获得锁 3).boolean tryLock() :尝试获取锁,如果获取成功返回true,反之返回false。也就是说,这个方法无论如何都不会阻塞等待获取锁。 4).boolean tryLock(long time, TimeUnit unit) :等待time时间,如果在time时间内获取到锁返回true,如果阻塞等待time时间内没有获取到锁返回false 5).unlock() : 释放锁
我们把大林小林吃冰棍的问题,改写为使用ReentrantLock实现代码如下:
//儿子接口类
class Son implements Runnable {
private IceBox iceBox; //冰箱的引用
private Lock lock; //Lock对象
private int bigSonNum = 0; //大儿子吃的数量
private int smallSonNum = 0; //小儿子吃的数量
public Son(IceBox iceBox) {
this.iceBox = iceBox;
lock = new ReentrantLock();
}
@Override
public void run() {
while(true){
try {
if(lock.tryLock(10, TimeUnit.SECONDS)){ //尝试获得锁。
if (IceBox.iceScreamNumber <= 0) { //冰棍吃完了。。。。
break; //退出循环,也就意味着线程结束...
}
IceBox.iceScreamNumber--;
if ("大林".equals(Thread.currentThread().getName())) {
System.out.println("大林吃了一根冰棍,还剩:"+IceBox.iceScreamNumber+"根。");
bigSonNum++;
} else {
System.out.println("小林吃了一根冰棍,还剩:"+IceBox.iceScreamNumber+"根。");
smallSonNum++;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
//释放锁
lock.unlock();
}
}
}
public void showResult() {
System.out.println("大林吃了:" + bigSonNum + "根。");
System.out.println("小林吃了:" + smallSonNum + "根。");
}
}
//冰箱类
public class IceBox {
public static int iceScreamNumber = 50; //冰箱里面的50根冰棍
public static void main(String[] args) {
IceBox iceBox = new IceBox(); //冰箱对象。
Son son = new Son(iceBox); //Runnable对象
Thread bigTh = new Thread(son, "大林"); //大儿子线程对象
Thread smallTh = new Thread(son, "小林"); //小儿子线程对象。
bigTh.start(); //启动大儿子线程
smallTh.start();//启动小儿子线程
try {
bigTh.join(); //等待两个线程结束
smallTh.join();
} catch (Exception ex) {
ex.printStackTrace();
}
son.showResult(); // 显示结果
}
}
执行结果:
大林吃了一根冰棍,还剩:49根。
大林吃了一根冰棍,还剩:48根。
大林吃了一根冰棍,还剩:47根。
大林吃了一根冰棍,还剩:46根。
大林吃了一根冰棍,还剩:45根。
大林吃了一根冰棍,还剩:44根。
大林吃了一根冰棍,还剩:43根。
大林吃了一根冰棍,还剩:42根。
大林吃了一根冰棍,还剩:41根。
大林吃了一根冰棍,还剩:40根。
大林吃了一根冰棍,还剩:39根。
大林吃了一根冰棍,还剩:38根。
大林吃了一根冰棍,还剩:37根。
大林吃了一根冰棍,还剩:36根。
大林吃了一根冰棍,还剩:35根。
大林吃了一根冰棍,还剩:34根。
大林吃了一根冰棍,还剩:33根。
大林吃了一根冰棍,还剩:32根。
大林吃了一根冰棍,还剩:31根。
大林吃了一根冰棍,还剩:30根。
大林吃了一根冰棍,还剩:29根。
大林吃了一根冰棍,还剩:28根。
大林吃了一根冰棍,还剩:27根。
大林吃了一根冰棍,还剩:26根。
大林吃了一根冰棍,还剩:25根。
大林吃了一根冰棍,还剩:24根。
大林吃了一根冰棍,还剩:23根。
大林吃了一根冰棍,还剩:22根。
小林吃了一根冰棍,还剩:21根。
小林吃了一根冰棍,还剩:20根。
小林吃了一根冰棍,还剩:19根。
小林吃了一根冰棍,还剩:18根。
小林吃了一根冰棍,还剩:17根。
小林吃了一根冰棍,还剩:16根。
小林吃了一根冰棍,还剩:15根。
小林吃了一根冰棍,还剩:14根。
小林吃了一根冰棍,还剩:13根。
小林吃了一根冰棍,还剩:12根。
小林吃了一根冰棍,还剩:11根。
小林吃了一根冰棍,还剩:10根。
小林吃了一根冰棍,还剩:9根。
小林吃了一根冰棍,还剩:8根。
小林吃了一根冰棍,还剩:7根。
小林吃了一根冰棍,还剩:6根。
小林吃了一根冰棍,还剩:5根。
小林吃了一根冰棍,还剩:4根。
小林吃了一根冰棍,还剩:3根。
小林吃了一根冰棍,还剩:2根。
小林吃了一根冰棍,还剩:1根。
小林吃了一根冰棍,还剩:0根。
大林吃了:28根。
小林吃了:22根。
注意:
上面代码的if(lock.tryLock(10, TimeUnit.SECONDS))是为了保证线程在10秒钟内获得锁,这是因为 System.out.println和sleep语句会占用CPU的时间。如果这个时间设置太短,会导致拿不到锁,那么在finally中释放锁就会抛出异常。为了保证能拿到锁,可以把if(lock.tryLock(10, TimeUnit.SECONDS)) 判断语句替换为lock.lock(); 这样更稳妥。