一、同步代码块
先贴代码:
public class MyRunnable implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true) {
if(ticket == 0){
break;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "卖了一张票,还剩" + ticket + "张");
}
}
}
}
public class Demo {
public static void main(String[] args) {
MyRunnable mr1 = new MyRunnable();
Thread t1 = new Thread(mr1);
Thread t2 = new Thread(mr1);
Thread t3 = new Thread(mr1);
t1.setName("#窗口一#");
t2.setName("##窗口二##");
t3.setName("###窗口三###");
t1.start();
t2.start();
t3.start();
}
}
此处代码与上篇博文(卖票案例实现)有所区别,为了引出需要讲解的内容。
不同之处在于,while循环的判断条件改为了true,增加了线程休眠;
问题:在如上代码的执行过程中,会出现重复票,负数票的情况。
原因就是,在休眠过程中,线程A的控制权给了其他线程(比方说是B),在这个过程中,线程B对共享数据ticket进行了修改,所以当A线程进行打印的时候,ticket已经不是当初那个ticket了,他变了。
而且很有可能在ticket置零的时候,还没进行判断(为零则终止循环),就被更改了数据值,导致死循环。
将if判断条件改为 <=0 , 则死循环不会出现,但是依旧存在重复票和负数票。
所以,如果在其他线程执行的时候,共享数据不允许被操作,那么这种情况就会得以解决。
这就需要用到 synchronized() 方法。将操作共享数据的多条代码锁起来,实现同步代码块。
synchronized(任意对象){
操作共享数据的多条语句
}
默认情况下,锁是打开的,只要有一条线程去执行代码,所就会关闭。
当线程执行完出来了,锁才会自动打开。
同步的好处和弊端:
- 好处:解决了多线程的数据安全问题。
- 弊端:当线程很多时,所有的线程都会区判断同步上的锁,这很耗费资源,无形中会降低程序执行的效率。
二、锁对象唯一
上述卖票案例所用的实现方式是继承Runnable接口,线程之间使用的是同一个参数,所以锁也是共享的。
当每个线程都有自己的锁的时候,还是会出现重复票和负数票的问题,即线程变得不安全。(继承Thread类的方式实现多线程时)
public class MyThread extends Thread {
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized(obj){
if(ticket <= 0){
break;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "卖了一张票,还剩" + ticket + "张");
}
}
}
}
}
public class Demo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("窗口一");
t2.setName("窗口二");
t1.start();
t2.start();
}
}
这是需要将MyThread类中的ticket变量和Object对象设置为静态,这样所有的MyThread类对象共享一个静态变量。
三、同步方法
同步方法的锁对象是:this
同步静态方法的锁对象是:类.class
public class MyRunnable implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true) {
if("窗口一".equals(Thread.currentThread().getName())){
//同步方法
boolean res = synchronizedMethod();
if(res){
break;
}
}
if("窗口二".equals(Thread.currentThread().getName())){
//同步代码块
synchronized(this){
if(ticket == 0){
break;
}else {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "卖了一张票,还剩" + ticket + "张");
}
}
}
}
}
private synchronized boolean synchronizedMethod() {
if (ticket == 0) {
return true;
} else {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "卖了一张票,还剩" + ticket + "张");
return false;
}
}
}
public class Demo {
public static void main(String[] args) {
MyRunnable mr1 = new MyRunnable();
Thread t1 = new Thread(mr1);
Thread t2 = new Thread(mr1);
t1.setName("窗口一");
t2.setName("窗口二");
t1.start();
t2.start();
}
}
注意:此代码实现多线程的方式是继承Runnable接口,两个线程的参数相同,执行的都是MyRunnable的对象mr1中的run()方法,所以锁对象this是相同的。
同步静态方法:
public class MyRunnable implements Runnable{
//静态方法只能访问静态变量,所以要加上static进行修饰
private static int ticket = 100;
@Override
public void run() {
while (true) {
if("窗口一".equals(Thread.currentThread().getName())){
//同步方法
boolean res = synchronizedMethod();
if(res){
break;
}
}
if("窗口二".equals(Thread.currentThread().getName())){
//同步代码块
//这里的对象需要进行修改
synchronized(MyRunnable.class){
if(ticket == 0){
break;
}else {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "卖了一张票,还剩" + ticket + "张");
}
}
}
}
}
private static synchronized boolean synchronizedMethod() {
if (ticket == 0) {
return true;
} else {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "卖了一张票,还剩" + ticket + "张");
return false;
}
}
}
四、Lock
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作Lock中提供了获得锁和释放锁的方法
- void lock():获得锁
- void unlock()∶释放锁
Lock是接口,不能直接实例化,这里采用它的实现类ReentrantLock来实例化。
import java.util.concurrent.locks.ReentrantLock;
public class MyThread extends Thread {
private static int ticket = 100;
// private static Object obj = new Object();
ReentrantLock rlock = new ReentrantLock();
@Override
public void run() {
while (true) {
// synchronized(obj){
try {
rlock.lock();
if (ticket <= 0) {
break;
} else {
Thread.sleep(100);
ticket--;
System.out.println(Thread.currentThread().getName() + "卖了一张票,还剩" + ticket + "张");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rlock.unlock();
}
// }
}
}
}
五、死锁
死锁是因为锁的嵌套导致的,建议不要写锁的嵌套,这样可以避免死锁。