Android开发笔记(八十八)同步与加锁

同步synchronized

同步方法

synchronized可用来给方法或者代码块加锁,当它修饰一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。这就意味着,当两个并发线程同时访问synchronized代码块时,两个线程只能是排队做串行处理,另一个线程要等待前一个线程执行完该代码块后,才能再次执行synchronized代码块。

使用synchronized修饰某个方法,该方法便成为一个同步方法,在同一时刻只能有一个线程执行该方法。可是,synchronized的锁机制太重量级,不但整个同步方法的代码都加锁,就连该方法用到的所有类变量也一并加锁。因此,同步方法覆盖的代码越多,加锁操作对效率的影响就越严重。

显式指纹(同步代码块)

为缩小同步方法的影响方法,我们可让synchronized只修饰某个代码块,而不必修饰整个方法,synchronized修饰后的代码块叫做同步代码块。同步代码块要先指定该代码块的密钥对象,这个对象可以是任意相关类的实例,它相当于一个指纹,每个线程执行同步代码块时都要先验证指纹,指纹相同的线程进入同一个队列依次排队,若指纹不同则进入另外的执行队列。

下面是同步方法和同步代码块的代码示例:

public class TestCode {

	public static void main(String[] args) {
		TestCode1 t1 = new TestCode1();
		TestCode2 t2 = new TestCode2();
		TestCode3 t3 = new TestCode3();
		Thread ta = new Thread(t1, "A");
		Thread tb = new Thread(t2, "B");
		Thread tc = new Thread(t3, "C");
		ta.start();
		tb.start();
		tc.start();
	}

	private static TestCode test = new TestCode();

	public static class TestCode1 implements Runnable {
		public synchronized void run() {
			for (int i = 0; i < 5; i++) {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()+" synchronized loop "+i);
			}
		}
	}

	public static class TestCode2 implements Runnable {
		public void run() {
			synchronized (test) {  //这里如果使用this则表示新实例,就不与其他代码块搞在一起
				for (int i = 0; i < 5; i++) {
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName()+" synchronized loop "+i);
				}
			}
		}
	}

	public static class TestCode3 implements Runnable {
		public void run() {
			synchronized (test) {  //这里如果使用this则表示新实例,就不与其他代码块搞在一起
				for (int i = 0; i < 5; i++) {
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName()+" synchronized loop "+i);
				}
			}
		}
	}

}

下面是该示例代码的执行结果:

B synchronized loop 0
A synchronized loop 0
A synchronized loop 1
B synchronized loop 1
B synchronized loop 2
A synchronized loop 2
B synchronized loop 3
A synchronized loop 3
A synchronized loop 4
B synchronized loop 4
C synchronized loop 0
C synchronized loop 1
C synchronized loop 2
C synchronized loop 3
C synchronized loop 4

从输出结果可以看出,同步代码块如果加锁的是同一个对象实例,那么这两个同步代码块也被看作是互相排他的,同一时刻也只能有两个代码块的其中之一被执行,因此日志显示:线程B的同步代码块都执行完了,才开始执行线程B的同步代码块,即使两个代码块是在不同的地方。

隐式指纹

前面说到,synchronized会对同步代码内部的类变量加锁,这样一来,如果两个同步代码都使用了某个类变量,那也会产生排队等待的情况。因为某线程执行第一个同步代码时,会给类变量上锁;然后另一线程执行第二个同步代码,也准备给类变量上锁,结果发现类变量已经上锁无法再次上锁;所以后面的线程只好等待前面的线程执行完成。这种情况下,两个同步代码使用同一个类变量,我们可将其当作隐式指纹;而同步代码块事先指定密钥对象,可称作是显式指纹。

下面是隐式指纹的代码示例:

public class TestSynchronized {

	private int count = 0;

	public synchronized void plus() {
		System.out.println("begin plus count="+count);
		count++;
		try {
			Thread.sleep(2000);
			System.out.println("end plus count="+count);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public synchronized void minus() {
		System.out.println("begin minus count="+count);
		count--;
		System.out.println("end minus count="+count);
	}

	public void print() {
		try {
			Thread.sleep(1000);
			System.out.println("print count="+count);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) throws Exception {
		final TestSynchronized ts = new TestSynchronized();
		Thread thread_plus = new Thread("thread_plus") {
			public void run() {
				System.out.println("Thread:" + super.getName());
				ts.plus();
			}
		};

		final Thread thread_minus = new Thread("thread_minus") {
			public void run() {
				System.out.println("Thread:" + super.getName());
				//改变ts.sub和ts.print的执行顺序,看看是什么情况
				ts.minus();
				ts.print();
				// ts.minus();
			}
		};
		thread_plus.start();
		Thread.sleep(1000);
		thread_minus.start();
	}

}

下面是该示例代码的执行结果:

Thread:thread_plus
begin plus count=0
Thread:thread_minus
end plus count=1
begin minus count=1
end minus count=0
print count=0

从输出结果可以看出,minus线程总是在plus线程结束之后才开始执行,虽然两个线程的同步方法并没有指定加锁的对象,但两个方法内部都使用了类变量count,因此这两个方法成为了拥有同一个隐式指纹的排他方法。

加锁Lock

因为synchronized是重量级的加锁,对程序效率影响大,而且容易偏离预期,所以从jdk1.5开始,java引入了Lock接口,用来实现轻量级的加锁操作。Lock接口主要有两个派生类,分别是普通的重入锁ReentrantLock,以及读写锁ReentrantReadWriteLock。

重入锁ReentrantLock

ReentrantLock是不区分类型的普通锁,在lock与unlock之间的代码就是被锁保护的代码块。

ReentrantLock的常用方法如下:

lock : 加锁。该操作不允许中断,重复加锁时会一直等待。

unlock : 解锁。

lockInterruptibly : 允许中断的加锁。允许在等待时由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException

tryLock : 尝试锁。若之前未锁则加锁,并返回true;若之前已被当前线程锁,也返回true;若之前已被其他线程锁,则返回false。

getHoldCount : 获取当前线程的加锁次数。

isLocked : 判断是否加锁。

getQueueLength : 获取等待队列的长度。

读写锁ReentrantReadWriteLock

ReentrantReadWriteLock是区分了读锁和写锁的混合锁,读锁是共享锁,写锁是排它锁。

ReentrantReadWriteLock的常用方法如下:

readLock : 获得读锁对象ReentrantReadWriteLock.ReadLock

writeLock : 获得写锁对象ReentrantReadWriteLock.WriteLock

isWriteLocked : 判断是否加了写锁。

getReadHoldCount : 获取加读锁的次数。

getWriteHoldCount : 获取加写锁的次数。

getQueueLength : 获取等待队列的长度。

下面是ReentrantReadWriteLock.ReadLock的常用方法:

lock : 加读锁。

lockInterruptibly : 允许中断的加锁。

tryLock : 尝试加读锁。

unlock : 解除读锁。

下面是ReentrantReadWriteLock.WriteLock的常用方法:

lock : 加写锁。

lockInterruptibly : 允许中断的加锁。

tryLock : 尝试加写锁。

unlock : 解除写锁。

匿名内部类的加锁

匿名内部类使用synchronized要小心,虽然看起来同步代码只有一个,但是匿名内部类每次使用都是创建新类并实例化,所以多次使用匿名内部类其实是调用不同的类,不同类的内部同步方法,自然是互不影响的了。这就是匿名内部类时常令人迷惑的一个地方,遇到这种情况,建议采用Lock加锁,而不要用synchronized加锁。匿名内部类的说明参见《Android开发笔记(八十六)几个特殊的类》。

下面是同时采用两种加锁方式的示例代码:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.locks.ReentrantLock;

public class TestAnonymous {

	public static String getNowDateTime() {
		SimpleDateFormat s_format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		Date d_date = new Date();
		String s_date = "";
		s_date = s_format.format(d_date);
		return s_date;
	}

	private static void runSync() {
		for (int i = 0; i < 5; i++) {
			final int pos = i;
			Thread t = new Thread() {
				@Override
				public synchronized void run() {
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(getNowDateTime() + " runSync pos=" + pos);
				}
			};
			t.start();
		}
	}

	private final static ReentrantLock lock = new ReentrantLock();

	private static void runLock() {
		for (int i = 0; i < 5; i++) {
			final int pos = i;
			Thread t = new Thread() {
				@Override
				public void run() {
					try {
						lock.lock();
						Thread.sleep(1000);
						System.out.println(getNowDateTime() + " runLock pos=" + pos);
					} catch (InterruptedException e) {
						e.printStackTrace();
					} finally {
						lock.unlock();
					}
				}
			};
			t.start();
		}
	}

	public static void main(String[] args) {
		runSync();
		runLock();
	}

}

下面是该示例代码的执行结果:

2016-04-20 11:25:36 runSync pos=1
2016-04-20 11:25:36 runSync pos=4
2016-04-20 11:25:36 runSync pos=2
2016-04-20 11:25:36 runSync pos=3
2016-04-20 11:25:36 runSync pos=0
2016-04-20 11:25:36 runLock pos=0
2016-04-20 11:25:37 runLock pos=1
2016-04-20 11:25:38 runLock pos=2
2016-04-20 11:25:39 runLock pos=3
2016-04-20 11:25:40 runLock pos=4

从输出结果可以看出,在匿名内部类中,synchronized的加锁操作不符合预期结果,各线程仍是乱序执行。相比之下,Lock的加锁操作符合预期,各线程按顺序依次执行。

点此查看Android开发笔记的完整目录

时间: 2024-10-12 15:32:34

Android开发笔记(八十八)同步与加锁的相关文章

Android学习笔记(十八)——使用意图筛选器和实现浏览网页(附源码)

使用意图筛选器 点击下载源码 1.创建一个Intents项目,给该项目添加一个新类,命名为MyBrowserActivity,在res/layout文件夹下新增一个browser.xml: 2.在AndroidManifest.xml文件中添加如下代码: 添加权限: <uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="a

Android开发实战(十八):Android Studio 优秀插件:GsonFormat

原文:Android开发实战(十八):Android Studio 优秀插件:GsonFormat Android Studio 优秀插件系列: Android Studio 优秀插件(一):GsonFormat ------------------------------------------------------------------------------------------------------- 这几天没有活,于是乎整理了一些代码,顺便把一些一直在使用的东西也整理下,然后学

Android开发笔记(九十八)往图片添加部件

添加圆角 添加圆角的功能,要用到Canvas类的drawRoundRect方法,即把画布裁剪成指定的圆角矩形. 下面是给图片添加圆角的效果截图: 下面是给图片添加圆角的代码片段: public static Bitmap getRoundImage(Bitmap bitmap, int roundPixels) { //创建一个和原始图片一样大小位图 Bitmap roundConcerImage = Bitmap.createBitmap(bitmap.getWidth(), bitmap.g

Android基础笔记(十八)- Fragment

博客的感悟终点-开始 什么是Fragment 添加fragment到Activity的两种方式 Fragment的生命周期 Fragment的向下兼容 Fragment之间的通信 博客的感悟,终点-开始 这个是基础的最后一篇博客了,学习了很多,也有很多感触. 就在这里大致总结一下. 坚持往往很难,完美的坚持下去更难.这是写这十八篇博客的感悟. 时间流失的很快,总是感觉时间不够用.慢慢的就会让自己博客的质量下降.今天反思了一下,我这样不就是在制造"破窗户"吗?(破窗户理论不知道的可以去看

【转】Pro Android学习笔记(十八):用户界面和控制(6):Adapter和AdapterView

目录(?)[-] SimpleCursorAdapter 系统预置的layout ArrayAdapter 动态数据增插删排序 自定义TextView风格 其他Adapter AdapterView不仅仅是UI,同时还将数据关联到UI上,例如在手机中经常使用的ListView就是AdapterView. ListView.GridView.Spinner和Gallery都是AdapterView,AdapterView是ViewGroup,也就是容器,含有多个UI布局相同的子view.对于Ada

Android开发系列(十八):自定义控件样式在drawable文件夹下的XML实现

在Android开发的过程中,我们经常需要对控件的样式做一下改变,可以通过用增加背景图片的方式进行改变,但是背景图片放多了肯定会使得APK文件变的很大. 我们可以用自定义属性shape来实现. shape: gradient   -- 对应颜色渐变. startcolor.endcolor就不多说了. android:angle 是指从哪个角度开始变. solid      --  填充. stroke   --  描边. corners  --  圆角. padding   -- 定义内容离边

Android开发系列(十八):自己定义控件样式在drawable目录下的XML实现

在Android开发的过程中,我们常常须要对控件的样式做一下改变,能够通过用添加背景图片的方式进行改变,可是背景图片放多了肯定会使得APK文件变的非常大. 我们能够用自己定义属性shape来实现. shape: gradient   -- 相应颜色渐变. startcolor.endcolor就不多说了. android:angle 是指从哪个角度開始变. solid      --  填充. stroke   --  描边. corners  --  圆角. padding   -- 定义内容

Android学习笔记(十八)——再谈升级数据库

//此系列博文是<第一行Android代码>的学习笔记,如有错漏,欢迎指正! 之前我们为了保证数据库中的表是最新的,只是简单地在 onUpgrade()方法中删除掉了当前所有的表,然后强制重新执行了一遍 onCreate()方法.这种方式在产品的开发阶段确实可以用,但是当产品真正上线了之后就绝对不行了.想象以下场景,比如你编写的某个应用已经成功上线,并且还拥有了不错的下载量.现在由于添加新功能的原因,使得数据库也需要一起升级,然后用户更新了这个版本之后发现以前程序中存储的本地数据全部丢失了.

Android学习笔记(十八):ListView和RatingBar

在学习笔记(十七)中,我们对ListView做了进一步的探讨,然而给出的例子list中的元素可以有多个widget,并可灵活设置他们的值,但是这些widget之间缺乏互动,而且getView()的调用,需要重刷给list的entry,我们希望能够在entry中触发变化. 本次,我们继续根据<Beginging Android 2>的学习,结合RatingBar,将程序稍微复杂一点.RatingBar看用于媒体库的平级,我们用RatingBar取代了之前例子的图标,当RatingBar设置为三星

Xamarin.Android开发实践(十八)

Xamarin.Android之SlidingMenu 一.前言 有位网友在评论中希望能够出个在Xamarin.Android下实现SlidingMenu效果的随笔,刚好昨天在观看官网示例项目的时候也看到这个SlidingMenu,但是最终的效果并不是我们所期待的,至此笔者就在官方的论坛中寻找,最后也成功的寻找到的答案,下面笔者将带领带领大家实现SlidingMenu. 二.准备工作 实现SlidingMenu重点是需要一个第三方的类库,笔者已经把部分重要的方法注释了,下面是下载地址: 从Git