一、多线程给编码带来的影响
下面我们看一段代码,这段代码的含义是模拟现实生活中的卖票:
/**
*
* 类描述
* Copyright © 2017永乐科技. All rights reserved.
* <p>@Title: ThreadTicket.java <p>
* <p>@Package: demo.com.test.thread <p>
* <p>@author: keep_trying <p>
* <p>@date: 2017年12月15日 下午4:24:25 <p>
* <p>@version: V1.0 <p>
*/
public class ThreadTicket implements Runnable{
//共享资源
private int ticketNums = 10;
public static void main(String[] args) {
ThreadTicket t = new ThreadTicket();
new Thread(t).start();//窗口1
new Thread(t).start();//窗口2
new Thread(t).start();//窗口3
new Thread(t).start();//窗口4
}
@Override
public void run() {
//为什么要一个while循环呢? 因为每一个窗口都在不停的卖票
while (true) {
try {
//休眠10毫秒是等待两个线程同时获取ticketNums总张数
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果票的剩余张数为0的话停止卖禁票
if (ticketNums==0) {
break;
}
System.out.println(Thread.currentThread().getName() + "剩余票数为" +ticketNums--);
}
}
}
运行结果1:
Thread-2剩余票数为7
Thread-1剩余票数为8
Thread-3剩余票数为9
Thread-0剩余票数为10
Thread-2剩余票数为6
Thread-1剩余票数为5
Thread-3剩余票数为4
Thread-0剩余票数为3
Thread-2剩余票数为2
Thread-3剩余票数为0
Thread-0剩余票数为1
Thread-1剩余票数为0
Thread-2剩余票数为-1
我们发现票竟然卖出了负数,这是不符合现实场景的。
二、分析原因
看一下线程的执行流程:
T1时间线程1和线程2同时得到ticketNums = 1 ,线程1获得了cpu的执行权(此时线程2等待)执行ticketNums -1操作,此时ticketNums =0,线程1把工作内存中的值更新到主内存,此时ticketNums =0。线程1执行完操作,线程2 获得了cpu的执行权,之前他已经判断了ticketNums =1,所以他从主内存中获取ticketNums 的值为0,并且执行ticketNums -1操作,此时ticketNums 变成了-1。此过程是并发执行的,即线程1 和线程2 在同时在执行。
三、如何解决线程并发问题
synchronized 关键字可以保证多线程并发执行的顺序性,下面看改进后的代码:
/**
*
* 类描述
* Copyright © 2017永乐科技. All rights reserved.
* <p>@Title: ThreadTicket.java <p>
* <p>@Package: demo.com.test.thread <p>
* <p>@author: keep_trying <p>
* <p>@date: 2017年12月15日 下午4:24:25 <p>
* <p>@version: V1.0 <p>
*/
public class ThreadTicket implements Runnable{
//共享资源
private int ticketNums = 10;
public static void main(String[] args) {
ThreadTicket t = new ThreadTicket();
new Thread(t).start();//窗口1
new Thread(t).start();//窗口2
new Thread(t).start();//窗口3
new Thread(t).start();//窗口4
}
@Override
public void run() {
//为什么要一个while循环呢? 因为每一个窗口都在不停的卖票
while (true) {
//加入synchronized关键字
synchronized (this) {
try {
//休眠10毫秒是等待两个线程同时获取ticketNums总张数
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果票的剩余张数为0的话停止卖禁票
if (ticketNums<=0) {
break;
}
System.out.println(Thread.currentThread().getName() + "剩余票数为" +ticketNums--);
}
}
}
}
运行结果2:
Thread-0剩余票数为10
Thread-0剩余票数为9
Thread-0剩余票数为8
Thread-0剩余票数为7
Thread-0剩余票数为6
Thread-0剩余票数为5
Thread-0剩余票数为4
Thread-0剩余票数为3
Thread-0剩余票数为2
Thread-0剩余票数为1
运行结果没有出现-1,是我们想要的结果,那在来看下线程的执行流程:
在T1时间 线程1获取cpu的执行权 将ticketNums-1,此时ticketNums=0,在执行过程中线程2一会处于阻塞状态。线程2发现ticketNums=0 直接退出。此操作是串行执行的,即线程1执行完之后线程2才开始执行。
四、synchronized
synchronized:Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
synchronized原理
在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。
当我们调用某对象的synchronized方法时,就获取了该对象的同步锁。例如,synchronized(obj)就获取了“obj这个对象”的同步锁。
不同线程对同步锁的访问是互斥的。也就是说,某时间点,对象的同步锁只能被一个线程获取到!通过同步锁,我们就能在多线程中,实现对“对象/方法”的互斥访问。 例如,现在有两个线程A和线程B,它们都会访问“对象obj的同步锁”。假设,在某一时刻,线程A获取到“obj的同步锁”并在执行一些操作;而此时,线程B也企图获取“obj的同步锁” —— 线程B会获取失败,它必须等待,直到线程A释放了“该对象的同步锁”之后线程B才能获取到“obj的同步锁”从而才可以运行。
可以使用的地方
● 实例方法【1】
● 实例方法中代码块【2】
● 静态方法【3】
● 静态方法中代码块【4】
修饰实例方法
public class Synchronized {
private int x=100;
public synchronized void add(){
x++;
}
}
实例方法中代码块
public class Synchronized {
private int x=100;
public void add(){
synchronized(this){
x++;
}
}
}
修饰静态方法
public class Synchronized {
private static int x=100;
public static synchronized void add(){
x++;
}
}
静态方法中代码块
public class Synchronized {
private static int x=100;
public static synchronized void add(){
synchronized(Synchronized.class){
x++;
}
}
}
synchronized使用总结
先看几段代码
//代码1
public class SynchronizedTest implements Runnable {
public static void main(String[] args) {
SynchronizedTest sync = new SynchronizedTest();
new Thread(sync).start();;
new Thread(sync).start();;
}
@Override
public void run() {
print();
}
public synchronized void print(){
System.out.println(Thread.currentThread().getName()+"获得了锁,sleep 5 秒");
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
Thread-0获得了锁,sleep 5 秒
2017-12-20 14:30:58
Thread-1获得了锁,sleep 5 秒 //5秒后打印
2017-12-20 14:31:03 //5秒后打印
//代码2
public class SynchronizedTest implements Runnable {
public static void main(String[] args) {
SynchronizedTest sync1 = new SynchronizedTest();
SynchronizedTest sync2 = new SynchronizedTest();
new Thread(sync1).start();;
new Thread(sync2).start();;
}
@Override
public void run() {
print();
}
public synchronized void print(){
System.out.println(Thread.currentThread().getName()+"获得了锁,sleep 5 秒");
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
Thread-1获得了锁,sleep 5 秒 //同时打印
Thread-0获得了锁,sleep 5 秒 //同时打印
2017-12-20 14:38:24
2017-12-20 14:38:24
//代码3
public class SynchronizedStaticTest implements Runnable {
public static void main(String[] args) {
SynchronizedStaticTest sync1 = new SynchronizedStaticTest();
SynchronizedStaticTest sync2 = new SynchronizedStaticTest();
new Thread(sync1).start();
new Thread(sync2).start();
}
@Override
public void run() {
print();
}
public static synchronized void print(){
System.out.println(Thread.currentThread().getName()+"获得了锁,sleep 5 秒");
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
Thread-0获得了锁,sleep 5 秒
2017-12-20 14:57:23
Thread-1获得了锁,sleep 5 秒 //5秒后打印
2017-12-20 14:57:28 //5秒后打印
//代码4
public class SynchronizedTest implements Runnable {
public static void main(String[] args) {
SynchronizedTest sync = new SynchronizedTest();
new Thread(sync).start();;
new Thread(sync).start();;
}
@Override
public void run() {
print();
}
public synchronized void print(){
System.out.println(Thread.currentThread().getName()+"获得了锁,sleep 5 秒 print方法");
System.out.println(Thread.currentThread().getName()+"时间"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
output();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void output(){
System.out.println(Thread.currentThread().getName()+"获得了锁,sleep 5 秒 output方法");
}
}
运行结果:
Thread-1获得了锁,sleep 5 秒 print方法
Thread-1时间2017-12-20 15:08:42
Thread-1获得了锁,sleep 5 秒 output方法
Thread-0获得了锁,sleep 5 秒 print方法 //5秒后打印
Thread-0时间2017-12-20 15:08:47 //5秒后打印
Thread-0获得了锁,sleep 5 秒 output方法 //5秒后打印
通过代码1、代码2 我们可以得出:synchronized修饰实例方法是对类的当前实例进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块,注意这里是“类的当前实例”, 类的两个不同实例就没有这种约束了。
通过代码3 我们可以得出:synchronized修饰静态方法对类进行加锁,限制多线程同时访问该类的所有实例。代码3创建了两个对象,线程1获得了SynchronizedStaticTest类的锁(可以理解为全局锁),此时线程2只能等待。
通过代码4 我们可以得出:synchronized修饰实例方法,如果一个线程获得和对象锁,线程在调用其它用synchronized修饰的方法时会直接执行,不会等待继续获取锁 ,这是重入锁,此时对象锁的计数器为2。
实际上,在类中某方法或某代码块中有 synchronized,那么在生成一个该类实例后,改类也就有一个监视块,而static synchronized则是所有该类的实例公用一个监视快了,这也就是两个的区别了,也就是synchronized相当于 this.synchronized,而static synchronized相当于Class.synchronized.
总结:当synchronized作用在方法上时,锁住的便是对象实例(this);当作用在静态方法时锁住的便是对象对应的Class,静态方法锁相当于该类的一个全局锁;