1. 并发访问带来的线程安全问题:
1) 设想当多个线程刚好在同时时间访问一个公共资源时会怎么样?
2) 如果仅仅是读取那个资源那没什么问题,但如果要修改呢?同时修改必然会发生冲突导致数据不一致的错误(最典型的就是同时写一个文件);
3) 在实际问题中最典型的就是银行取钱问题,如果多人刚好同时用同一个账号取钱就会发生错误,而这种错误往往是非常严重的错误;
4) 因此要提供一种同步机制,即多线程访问临界资源时(对临界数据进行修改时)必须要同步进行;
5) Java提供了三种方法来实施线程同步,根据具体情况和需求选择一种进行同步:同步监视器(同步代码块)、同步方法、同步锁;
6) 但所有同步机制的原理都是这三步:加锁(锁定临界资源) -> 修改(修改临界资源) -> 释放锁(释放对临街资源的占用权);
2. 同步监视器——同步代码块:
1) 即对并发访问的临界资源用关键字synchronized进行限定,将其设定为同步监视器,而其后花括号中包含的代码就是同步代码块:
synchronized (Object obj) {
... // 同步代码块
}
2) 这个语法的意思就是进入代码块之前先将资源obj锁定住,只有锁定住资源obj的线程才有资格执行后面的代码块,代码块执行完毕之后线程就释放了对资源obj的锁定,然后obj就可以被其它线程锁定并使用;
3) 通俗地讲就是只有获取对obj的锁定之后才能用后面代码块中的代码访问obj资源,否则就无法访问obj也无法执行后面的代码,只能让线程停滞在那里等待其它线程解除对obj的锁定;
4) 同步代码块也被称为临界区,即多线程修改共享资源(临界资源)的代码区;
5) 这个机制就保证了并发线程在任意时刻只能有一个线程可以进入临界区;
3. 同步方法——构造线程安全类:
1) 如果将关键字synchronized作用于类的非静态方法上(即成员方法)那么就相当于对this锁定成同步监视器;
!!记住:synchronized关键字必须要指定一样东西作为同步监视器,静态方法没有this参数,因此无法被指定为同步方法;
2) 例如:
class A {
public synchronized void set(int val) {
this.val = val;
}
}
A a = new A();
a.set(10);
!最后的a.set(10);完全等价于
synchronized (a) {
a.set(10);
}
!其实Java编译器底层做的也是这种简单的宏替换;
3) 对于那些不可变类(即数据成员永远都不会改变)永远是线程安全的,而对于那些可变类,特别是可以修改数据成员的方法如果在多线程环境下使用可能会造成同步安全问题,因此如果要在多线程环境下运行就必须将这些可以修改数据的方法用synchronized关键字修饰成同步方法,那么这样的类就是线程安全的类;
4) 如何高效地做到线程安全?
i. 同步方法的执行效率一定低于不同步方法,这是显然的,因为同步方法发生访问冲突时是需要等待的,而非同步方法无需等待;
ii. 因此只将那些需要修改临界数据的方法进行同步,不修改数据的方法保持原样就能提高线程安全的执行效率(比如银行存取中查看账号这样的操作就不需要同步,因为账号是不变的,但取钱这种修改临界数据的操作就需要同步了);
5) 如果可变类有两种运行环境(一种单线程一种多线程)则应该提供两种实现版本,一种是线程不安全版本供单线程使用以提高运行效率,另一种是线程安全版本供多线程环境使用;
!!例如Java的StringBuilder就是线程不安全的,专门供单线程环境使用,而StringBuffer是线程安全的,供多线程环境使用;
4. 释放同步锁:
1) 如何释放同步代码块和同步方法对同步监视器的锁定呢?
2) 有下列4中情况:
i. 同步代码块或方法执行完毕;
ii. 遇到break、return等;
iii. 执行期间出现未处理的错误或异常;
iv. 执行期间调用了同步监视对象(监视器)的wait方法使当前线程暂停并释放同步锁;
3) 下列两种情况不会释放同步锁,因此容易导致死锁,一定要慎用:
i. sleep和yield,虽然暂停了,但占着锁不释放;
ii. 调用该线程的suspend挂起,同样不释放同步锁;
5. 死锁:
1) 即两个线程相互等待对方释放资源锁(即刚好A线程要用到B线程锁定的一个资源,而B线程中刚好也要用到A线程中锁定的一个资源),两个线程相互等待没完没了;
2) 任何操作系统都无法完全避免死锁,所以编程时一定要注意,特别是在同步锁特别多的情况下死锁多发;
3) 示例:
public class Test {
A a = new A();
B b = new B();
class A implements Runnable {
public synchronized void foo(B b) {
System.out.println("In A try call B");
b.bar(this);
System.out.println("A end!");
}
@Override
public void run() {
// TODO Auto-generated method stub
foo(b);
}
}
class B implements Runnable {
public synchronized void bar(A a) {
System.out.println("In B try cal A");
a.foo(this);
System.out.println("B end!");
}
@Override
public void run() {
// TODO Auto-generated method stub
b.bar(a);
}
}
public void init() {
Thread ta = new Thread(a);
Thread tb = new Thread(b);
ta.start();
tb.start();
}
public static void main(String[] args) {
new Test().init();
}
}
!发生死锁,都在输出End之前进入死锁,相互等待;
!!像有些方法(suspend等)容易发生死锁,应该尽量不要用;
5. 同步锁:
1) 即Lock接口及其子接口所代表的对象;
2) 它是一种显式锁,它自己本身就是同步监视对象,它的用法是和try-final块搭配使用:
Lock lock = new Lock();
// 其它代码
lock.lock(); // 上锁
try {
// 同步代码块
}
finally {
// 善后操作
lock.unlock(); // 开锁
}
3) 它的两个方法(一个上锁一个开锁):
i. void lock(); // 上锁
ii. void unlock(); // 开锁
4) 把try块想象成一间屋子,同时只能让一个人进去,因此进去的那个人必须先获取锁打开门,只有当他使用完毕后再后交出锁让其他人用这间屋子,Lock对象本身将作为监视器了;
5) 一般多用Lock的实现类ReentrantLock,即可重入锁,可以在锁住的代码块中(try块)嵌套加锁,但是这种嵌套加锁的结构要求解锁时一定要按照反向顺序解锁,即内层锁先解,外层锁后解,否则会产生锁异常;