当多个线程并发访问同一个资源对象时,可能会出现线程不安全的问题,比如现有50个苹果,现在有请三个童鞋(小A,小B,小C)上台表演吃苹果.因为A,B,C三个人可以同时吃苹果,此时使用多线程技术来实现这个案例.
class Apple implements Runnable{
private int num = 50;//苹果总数
public void run() {
for (int i = 0; i < 50; i++) {
if (num > 0) {
try {
//模拟网络延迟
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "吃了编号为"
+ num-- + "的苹果");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
public class AppleEatingImplements {
public static void main(String[] args) {
//创建三个线程(三个同学),吃苹果
Runnable apple = new Apple();
new Thread(apple,"A童鞋").start();
new Thread(apple,"B童鞋").start();
new Thread(apple,"C童鞋").start();
}
}
为什么编号为39的苹果被吃了两次呢?
当A、B线程拿到编号为39的苹果时,打印出来,有一个还没来得及做num--,而有一个做了num减一操作,num还剩38,这时候线程进入睡眠状态。这时候C线程来了,打印38,做减1操作,睡眠……
要解决上述多线程并发访问多一个资源的安全性问题,就必须得保证打印苹果编号和苹果总数减1操作,必须同步完成.即是说,A线程进入操作的时候,B和C线程只能在外等着,A操作结束,A和B和C才有机会进入代码去执行.
解决多线程并发访问资源的安全问题,有三种方式:
方式1:同步代码块
方式2:同步方法
方式3:锁机制(Lock)
方式1:同步代码块:
语法:
synchronized(同步锁)
{
需要同步操作的代码
}
同步锁:
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制.也称为同步监听对象/同步锁/同步监听器/互斥锁。
实际上,对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁,谁拿到锁,谁就可以进入代码块,其他线程只能在代码块外面等着,而且注意,在任何时候,最多允许一个线程拥有同步锁.
Java程序运行可以使用任何对象作为同步监听对象,但是一般的,我们把当前并发访问的共同资源作为同步监听对象.
//同步代码块
class Apple2 implements Runnable{
private int num = 100;
public void run() {
for(int i = 0; i < 50; i ++){
//this表示Apple2对象,该对象属于多线程共享的资源
synchronized(this){
if(num>0){
System.out.println(Thread.currentThread().getName()+"吃了"+num-- +"个苹果");
}
}
}
}
}
public class SynchronizedBlockDemo {
public static void main(String[] args) {
Runnable a = new Apple2();
//三个线程表示三个人
new Thread(a,"小A").start();
new Thread(a,"小B").start();
new Thread(a,"小C").start();
}
}
方式2:同步方法:
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着.
Synchronized public void doWork(){
///TODO
}
同步锁是谁:
对于非static方法,同步锁就是this.
对于static方法,我们使用当前方法所在类的字节码对象(Apple2.class).
//同步方法
class Apple3 implements Runnable{
private int num = 50;
public void run() {
for(int i = 0; i < 50; i ++){
try {
doWork();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
synchronized private void doWork() throws InterruptedException {
if(num>0){
System.out.println(Thread.currentThread().getName()+"吃了"+num +"个苹果");
num --;
Thread.sleep(10);
}
}
}
public class SynchronizedMethodDemo {
public static void main(String[] args) {
Runnable a = new Apple3();
new Thread(a,"小A").start();//三个线程表示三个人
new Thread(a,"小B").start();
new Thread(a,"小C").start();
}
}
注意:
不要使用synchronized修饰run方法,修饰之后,某一个线程就执行完了所有的功能. 好比是多个线程出现串行.
解决方案:把需要同步操作的代码定义在一个新的方法中,并且该方法使用synchronized修饰,再在run方法中调用该新的方法即可.
实际上,同步代码块和同步方法差不了多少,在本质上是一样的,两者都用了一个关键字synchronized,synchronized保证了多线程并发访问时的同步操作,避免线程的安全性问题,但是有一个弊端,就是使用synchronized的方法/代码块的性能比不用要低一些,因此如果要用synchronized,建议尽量减小synchronized的作用域。
方式3:同步锁(锁机制)
Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象.
//锁机制
class Apple4 implements Runnable{
private int num = 50;
//创建锁对象
private final Lock lock = new ReentrantLock();
public void run() {
for(int i = 0; i < 50; i ++){
doWork();
}
}
private void doWork() {
//进入方法,立马加锁
lock.lock();//获取锁
try {
//注意:if要放到try里,不然num为0时就不进入if中,最后锁就释放不了了
if(num>0){
System.out.println(Thread.currentThread().getName()+"吃了"+num +"个苹果");
num--;
Thread.sleep(10);
}
} catch (Exception e) {
e.printStackTrace();
}finally{
//释放锁
lock.unlock();
}
}
}
public class LockDemo {
public static void main(String[] args) {
Runnable a = new Apple4();
new Thread(a,"小A").start();//三个线程表示三个人
new Thread(a,"小B").start();
new Thread(a,"小C").start();
}
}