多线程编程优点是执行效率高,缺点就是太容易出现错误情况,这是因为多条线程访问同一资源,而线程代码有多条同时Java线程调度具有随机性造成的,当然如果在编程时能很好的注意这些问题,其实也是可以解决的。
0.知识体系
1.线程安全问题
下述代码就是有安全问题,安全问题出现的原因是因为CPU切换线程的不确定性造成的。
class BankAccount
{
private int money;
public BankAccount(int money){
this.money = money;
}
public void getMoney(int num){
if(money > num){
money -= num;
System.out.println(Thread.currentThread().getName() + " 取钱 " + num +" 元;" + "余额 " + money);
}else{
System.out.println("余额不足");
System.exit(0);
}
}
}
class Customer implements Runnable
{
private BankAccount account = null;
public Customer(BankAccount account){
this.account = account;
}
public void run(){
while(true){
account.getMoney(200);
}
}
}
public class ThreadSynTest
{
public static void main(String[] args)
{
BankAccount account = new BankAccount(1000);
Customer ct1 = new Customer(account);
Customer ct2 = new Customer(account);
Thread t1 = new Thread(ct1);
Thread t2 = new Thread(ct2);
t1.start();
t2.start();
}
}
2.解决方案
1)同步代码块
上述代码 不具有同步安全性,两个并发程序在修改账户时,正好发生切换。为解决这个问题,多线程引入了同步监视器解决,使用同步监视器的通用方法是同步代码块。语法如下:
synchronized(obj){//obj就是同步监视器
//code
}
线程开始执行同步代码之前,必须先获得对同步监视器的锁定。任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码执行完成后,该线程释放对该同步监视器的锁定。阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。
利用同步代码块保持同步如下:
class BankAccount
{
private int money;
public BankAccount(int money){
this.money = money;
}
public void getMoney(int num){
if(money > num){
money -= num;
System.out.println(Thread.currentThread().getName() + " 取钱 " + num +" 元;" + "余额 " + money);
}else{
System.out.println("余额不足");
System.exit(0);
}
}
}
class Customer implements Runnable
{
private BankAccount account = null;
public Customer(BankAccount account){
this.account = account;
}
public void run(){
while(true){
try{Thread.sleep(100);}catch(InterruptedException ie){}
synchronized(account){//进入同步代码块之前必须先获得对account账户的锁定
account.getMoney(200);
}
}
}
}
public class ThreadSynTest
{
public static void main(String[] args)
{
BankAccount account = new BankAccount(1000);
Customer ct1 = new Customer(account);
Customer ct2 = new Customer(account);
Thread t1 = new Thread(ct1);
Thread t2 = new Thread(ct2);
t1.start();
t2.start();
}
}
2)同步方法
通过使用同步方法方便实现线程安全的类,如下特征:
(1)该类的对象可以被多个线程安全地访问
(2)每个线程调用该对象的任意方法之后都将得到正确结果。
(3)每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。
synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器,属性等。
class BankAccount
{
private int money;
public BankAccount(int money){
this.money = money;
}
public synchronized void getMoney(int num){
if(money > num){
money -= num;
System.out.println(Thread.currentThread().getName() + " 取钱 " + num +" 元;" + "余额 " + money);
}else{
System.out.println("余额不足");
System.exit(0);
}
}
}
class Customer implements Runnable
{
private BankAccount account = null;
public Customer(BankAccount account){
this.account = account;
}
public void run(){
while(true){
try{Thread.sleep(100);}catch(InterruptedException ie){}
synchronized(account){//进入同步代码块之前必须先获得对account账户的锁定
account.getMoney(200);
}
}
}
}
public class ThreadSynTest
{
public static void main(String[] args)
{
BankAccount account = new BankAccount(1000);
Customer ct1 = new Customer(account);
Customer ct2 = new Customer(account);
Thread t1 = new Thread(ct1);
Thread t2 = new Thread(ct2);
t1.start();
t2.start();
}
}
3)同步锁
Java5开始,Java提供了一种功能更强大的线程同步机制,通过显式定义同步锁对象来实现同步,在这种机制下,同步锁使用Lock对象充当。Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock实现允许更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁),Lock、ReadWriteLock是Java 5新提供的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类;为ReadWriteLock提供了ReentrantReadWriteLock实现类。
在实现线程安全控制中,比较常用的是ReentrantLock(可重入锁),使用该Lock对象可以显式地加锁、释放锁,通常使用ReentrantLock代码如下:
class className
{
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
//定义需要保证线程安全的方法
public void methodName()
{
//加锁
lock.lock();
try{
//需要保证线程安全的代码
}finally{
lock.unlock();
}
}
}
使用同步锁后的代码如下:
import java.util.concurrent.locks.ReentrantLock;
class BankAccount
{
private ReentrantLock lock = new ReentrantLock();
private int money;
public BankAccount(int money){
this.money = money;
}
public void getMoney(int num){
lock.lock();
try{
if(money > num){
money -= num;
System.out.println(Thread.currentThread().getName() + " 取钱 " + num +" 元;" + "余额 " + money);
}else{
System.out.println("余额不足");
System.exit(0);
}
}
finally{
lock.unlock();
}
}
}
class Customer implements Runnable
{
private BankAccount account = null;
public Customer(BankAccount account){
this.account = account;
}
public void run(){
while(true){
try{Thread.sleep(100);}catch(InterruptedException ie){}
synchronized(account){//进入同步代码块之前必须先获得对account账户的锁定
account.getMoney(200);
}
}
}
}
public class ThreadSynTest
{
public static void main(String[] args)
{
BankAccount account = new BankAccount(1000);
Customer ct1 = new Customer(account);
Customer ct2 = new Customer(account);
Thread t1 = new Thread(ct1);
Thread t2 = new Thread(ct2);
t1.start();
t2.start();
}
}
3.死锁研究
1)产生死锁的原因
下面是一个死锁例子:
class Locks
{
public static final Object lockA = new Object();
public static final Object lockB = new Object();
}
class PublicTarget
{
public static void showInfo(){
System.out.println("----" + Thread.currentThread().getName() + " 拿到两锁----");
}
}
class Accesser implements Runnable
{
private boolean flag = true;
public void run(){
if(flag){
synchronized(Locks.lockA){
flag = false;
System.out.println(Thread.currentThread().getName() + " 拿到lockA锁");
try{Thread.sleep(100);}catch(InterruptedException ex){}
synchronized(Locks.lockB){
System.out.println(Thread.currentThread().getName() + " 拿到lockB锁");
PublicTarget.showInfo();
}
}
}else{
synchronized(Locks.lockB){
flag = true;
System.out.println(Thread.currentThread().getName() + " 拿到lockB锁");
try{Thread.sleep(100);}catch(InterruptedException ex){}
synchronized(Locks.lockA){
System.out.println(Thread.currentThread().getName() + " 拿到lockA锁");
PublicTarget.showInfo();
}
}
}
}
}
public class DeadLockTest{
public static void main(String[] args){
Accesser accesser = new Accesser();
Thread a = new Thread(accesser);
Thread b = new Thread(accesser);
a.start();
b.start();
}
}
2)死锁的预防
待补充...