当多个线程并发访问同一个资源对象时,可能会出现线程不安全的问题,比如现有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{
-
<span style="white-space:pre"> </span>private int num = 50;
-
<span style="white-space:pre"> </span>public void run() {
-
<span style="white-space:pre"> </span>for(int i = 0; i < 50; i ++){
-
<span style="white-space:pre"> </span>try {
-
<span style="white-space:pre"> </span>doWork();
-
<span style="white-space:pre"> </span>} catch (InterruptedException e) {
-
<span style="white-space:pre"> </span>// TODO Auto-generated catch block
-
<span style="white-space:pre"> </span>e.printStackTrace();
-
<span style="white-space:pre"> </span>}
-
<span style="white-space:pre"> </span>}
-
<span style="white-space:pre"> </span>}
-
<span style="white-space:pre"> </span>synchronized private void doWork() throws InterruptedException {
-
<span style="white-space:pre"> </span>if(num>0){
-
<span style="white-space:pre"> </span>System.out.println(Thread.currentThread().getName()+"吃了"+num +"个苹果");
-
<span style="white-space:pre"> </span>num --;
-
<span style="white-space:pre"> </span>Thread.sleep(10);
-
<span style="white-space:pre"> </span>}
-
<span style="white-space:pre"> </span>
-
<span style="white-space:pre"> </span>
-
<span style="white-space:pre"> </span>}
-
}
-
public class SynchronizedMethodDemo {
-
<span style="white-space:pre"> </span>public static void main(String[] args) {
-
<span style="white-space:pre"> </span>Runnable a = new Apple3();
-
<span style="white-space:pre"> </span>new Thread(a,"小A").start();//三个线程表示三个人
-
<span style="white-space:pre"> </span>new Thread(a,"小B").start();
-
<span style="white-space:pre"> </span>new Thread(a,"小C").start();
-
<span style="white-space:pre"> </span>
-
<span style="white-space:pre"> </span>}
-
}
注意:
不要使用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();
-
}
-
}