一、Java 线程 同步与异步
多线程并发时,多个线程同时请求同一个资源,必然导致此资源的数据不安全,A线程修改了B线程的处理的数据,而B线程又修改了A线程处理的数理。显然这是由于全局资源造成的,有时为了解决此问题,优先考虑使用局部变量,退而求其次使用同步代码块,出于这样的安全考虑就必须牺牲系统处理性能,加在多线程并发时资源挣夺最激烈的地方,这就实现了线程的同步机制
1、 同步
A线程要请求某个资源,但是此资源正在被B线程使用中,因为同步机制存在,A线程请求不到,怎么办,A线程只能等待下去
2、异步
A线程要请求某个资源,但是此资源正在被B线程使用中,因为没有同步机制存在,A线程仍然请求的到,A线程无需等待
3、特点
显然,同步最最安全,最保险的。而异步不安全,容易导致死锁,这样一个线程死掉就会导致整个进程崩溃,但没有同步机制的存在,性能会有所提升。
二、 并发访问带来的线程安全问题
1) 设想当多个线程刚好在同时时间访问一个公共资源时会怎么样?
2) 如果仅仅是读取那个资源那没什么问题,但如果要修改呢?同时修改必然会发生冲突导致数据不一致的错误(最典型的就是同时写一个文件);
3) 在实际问题中最典型的就是银行取钱问题,如果多人刚好同时用同一个账号取钱就会发生错误,而这种错误往往是非常严重的错误;
4) 因此要提供一种同步机制,即多线程访问临界资源时(对临界数据进行修改时)必须要同步进行;
5) Java提供了三种方法来实施线程同步,根据具体情况和需求选择一种进行同步:同步监视器(同步代码块)、同步方法、同步锁;
6) 但所有同步机制的原理都是这三步:加锁(锁定临界资源) -> 修改(修改临界资源) -> 释放锁(释放对临街资源的占用权);
三、同步监视器——同步代码块
1) 即对并发访问的临界资源用关键字synchronized进行限定,将其设定为同步监视器,而其后花括号中包含的代码就是同步代码块:
2) 这个语法的意思就是进入代码块之前先将资源obj锁定住,只有锁定住资源obj的线程才有资格执行后面的代码块,代码块执行完毕之后线程就释放了对资源obj的锁定,然后obj就可以被其它线程锁定并使用;
3) 通俗地讲就是只有获取对obj的锁定之后才能用后面代码块中的代码访问obj资源,否则就无法访问obj也无法执行后面的代码,只能让线程停滞在那里等待其它线程解除对obj的锁定;
4) 同步代码块也被称为临界区,即多线程修改共享资源(临界资源)的代码区;
5) 这个机制就保证了并发线程在任意时刻只能有一个线程可以进入临界区;
public class DrawThread extends Thread
{
// 模拟用户账户
private Account account;
// 当前取钱线程所希望取的钱数
private double drawAmount;
public DrawThread(String name , Account account
, double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
// 当多条线程修改同一个共享数据时,将涉及数据安全问题。
public void run()
{
// 直接调用account对象的draw方法来执行取钱
// 同步方法的同步监视器是this,this代表调用draw()方法的对象。
// 也就是说:线程进入draw()方法之前,必须先对account对象的加锁。
account.draw(drawAmount);
}
}
3. 同步方法——构造线程安全类
1) 如果将关键字synchronized作用于类的非静态方法上(即成员方法)那么就相当于对this锁定成同步监视器;
2)记住:synchronized关键字必须要指定一样东西作为同步监视器,静态方法没有this参数,因此无法被指定为同步方法;
3) 对于那些不可变类(即数据成员永远都不会改变)永远是线程安全的,而对于那些可变类,特别是可以修改数据成员的方法如果在多线程环境下使用可能会造成同步安全问题,因此如果要在多线程环境下运行就必须将这些可以修改数据的方法用synchronized关键字修饰成同步方法,那么这样的类就是线程安全的类;
4) 如何高效地做到线程安全?
i. 同步方法的执行效率一定低于不同步方法,这是显然的,因为同步方法发生访问冲突时是需要等待的,而非同步方法无需等待;
ii. 因此只将那些需要修改临界数据的方法进行同步,不修改数据的方法保持原样就能提高线程安全的执行效率(比如银行存取中查看账号这样的操作就不需要同步,因为账号是不变的,但取钱这种修改临界数据的操作就需要同步了);
5) 如果可变类有两种运行环境(一种单线程一种多线程)则应该提供两种实现版本,一种是线程不安全版本供单线程使用以提高运行效率,另一种是线程安全版本供多线程环境使用;
!!例如Java的StringBuilder就是线程不安全的,专门供单线程环境使用,而StringBuffer是线程安全的,供多线程环境使用;
public class Account
{
// 封装账户编号、账户余额两个成员变量
private String accountNo;
private double balance;
public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
}
// 提供一个线程安全draw()方法来完成取钱操作
public synchronized void draw(double drawAmount)
{
// 账户余额大于取钱数目
if (balance >= drawAmount)
{
// 吐出钞票
System.out.println(Thread.currentThread().getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取钱失败!余额不足!");
}
}
// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
4. 释放同步监视器的锁定
1) 如何释放同步代码块和同步方法对同步监视器的锁定呢?
2) 有下列4中情况:
i. 同步代码块或方法执行完毕;
ii. 遇到break、return等;
iii. 执行期间出现未处理的错误或异常;
iv. 执行期间调用了同步监视对象(监视器)的wait方法使当前线程暂停并释放同步监视器
3) 下列两种情况不会释放同步监视器,因此容易导致死锁,一定要慎用:
i. sleep和yield,虽然暂停了,但占着锁不释放;
ii. 调用该线程的suspend挂起,同样不释放同步锁;
5. 同步锁lock
借鉴地址:http://www.cnblogs.com/dolphin0520/p/3923167.html
Lock对比synchronized有高手总结的差异如下:
总结来说,Lock和synchronized有以下几点不同:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
Lock的操作与synchronized相比,灵活性更高,而且Lock提供多种方式获取锁,有Lock、ReadWriteLock接口,以及实现这两个接口的ReentrantLock类、ReentrantReadWriteLock类。
public class Account
{
// 定义锁对象
private final ReentrantLock lock = new ReentrantLock();
// 封装账户编号、账户余额的两个成员变量
private String accountNo;
private double balance;
public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
}
// 提供一个线程安全draw()方法来完成取钱操作
public void draw(double drawAmount)
{
// 加锁
lock.lock();
try
{
// 账户余额大于取钱数目
if (balance >= drawAmount)
{
// 吐出钞票
System.out.println(Thread.currentThread().getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取钱失败!余额不足!");
}
}
finally
{
// 修改完成,释放锁
lock.unlock();
}
}
// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
6. 死锁:
1) 即两个线程相互等待对方释放资源锁(即刚好A线程要用到B线程锁定的一个资源,而B线程中刚好也要用到A线程中锁定的一个资源),两个线程相互等待没完没了;
2) 任何操作系统都无法完全避免死锁,所以编程时一定要注意,特别是在同步锁特别多的情况下死锁多发;
3) 它的两个方法(一个上锁一个开锁):
i. void lock(); // 上锁
ii. void unlock(); // 开锁
4) 把try块想象成一间屋子,同时只能让一个人进去,因此进去的那个人必须先获取锁打开门,只有当他使用完毕后再后交出锁让其他人用这间屋子,Lock对象本身将作为监视器了;
5) 一般多用Lock的实现类ReentrantLock,即可重入锁,可以在锁住的代码块中(try块)嵌套加锁,但是这种嵌套加锁的结构要求解锁时一定要按照反向顺序解锁,即内层锁先解,外层锁后解,否则会产生锁异常;