Java学习自我总结——线程同步的实现

线程同步的实现

线程同步的方式多种多样,下面我们一起来学习几种简单的实现方式,在这之前我们先写一个Bank类模拟银行存钱的操作,账户初始余额为100,每次存钱操作都会存入相对应的金额。

public class Bank{
		private int account = 100;
		
		public void deposit(int money) {
			account += money;
		}
		public int  getAccount() {
			return account;
		}
	}

再写一个线程实现了Runnable,它的run()方法会循环往账户里面存10块钱,循环1000次,存入金额10000元。先来看看非同步的情况下会发生什么,最后的结果往往会小于20100(因为简单的多线程发生错误的概率很低,所以在这里我通过大量的循环操作来增加出错的次数)。

public class Transfer implements Runnable{
	private Bank bank;
		
	private Transfer(Bank bank) {
		super();
		this.bank = bank;
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for (int i = 0; i < 1000; i++) {
			bank.deposit(10);
		}
	}
        public static void main(String[] args) {
                Bank bank = new Bank();
	        Transfer t1 = new Transfer(bank);
	        Transfer t2 = new Transfer(bank);
	        new Thread(t1).start();
	        new Thread(t2).start();
                try {
	            Thread.sleep(5000);
	            //延时五秒之后输出结果
	            System.out.println(bank.account);
	        } catch (InterruptedException e) {
	            // TODO Auto-generated catch block
	            e.printStackTrace();
	        }
        }		
}


方法一:使用方法实现线程同步

所谓同步方法,就是用synchronized关键字修饰的方法,每个java对象都有一个内置锁,当用synchronized修饰方法时,内置锁就会保护整个方法。注意:synchronized也可以用来修饰静态方法,此时调用该静态方法会锁住整个类。这个方法的实现很简单,只要修改Bank类的deposit()方法为:

public synchronized void deposit(int money) {
		account += money;
	}

方法二:使用代码块实现线程同步

修改Bank类的deposit()方法为:

public void deposit(int money) {
		synchronized (this) {
			account += money;
		}	
	}

方法三:使用重入锁实现线程同步

所谓重入锁,指的是以线程为单位,当一个线程获得对象锁之后,这个线程可以再次获取该对象锁,在本文,重入锁指的是ReentrantLock类,重新修改Bank类,其具体实现如下:
public class Bank{
	private int account = 100;
	//创建重入锁实例
	private Lock lock = new ReentrantLock();
	public void deposit(int money) {
		//打开锁
		lock.lock();
		try {
			account += money;
		} finally {
		//关闭锁
			lock.unlock();
		}}
}
值得注意的是, 使用完毕后一定要记得unlock()!!!否则会造成死锁,正常我们使用可以把它放到finally块中。

方法四:使用线程局部变量实现线程同步

使用ThreadLocal来管理变量,每个使用该变量的线程都会获得该变量的副本,副本之间相互独立,互不影响。常用的方法有get(),initialValue(),set(T value)等等。重新修改Bank类,具体实现如下:

public class Bank{
	//创建ThreadLocal变量
	private ThreadLocal<Integer> account = new ThreadLocal<Integer>() {
		@Override
		//变量初始化
		protected Integer initialValue() {
			// TODO Auto-generated method stub
			return 100;
		}			
	};	
	public  void deposit(int money) {
		//设置变量值
		account.set(account.get()+money);;
	}
	//获取变量值
	public int  getAccount() {
		return account.get();
	}
}
这时候在Transfer中两个线程account的值都是10100,想要获得正确的结果20100,需要进一步的处理。

方法五:使用阻塞队列实现线程同步

LinkedBlockingQueue<E>是一个FIFO的排序队列,其常用的方法有put(E e),向队尾增加一个元素,如果队列满则阻塞;size(),返回队列中的元素个数;take(),移出并返回队头元素,如果队列空则阻塞。编写一个Producer和Consumer类来实现。

private int size = 10;
private LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
private class Producer implements Runnable{

	@Override
	public void run() {
		// TODO Auto-generated method stub
		for (int i = 0; i <size; i++) {
			int b = new Random().nextInt(100);
			try {
				queue.put(b);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
}
private class Consumer implements Runnable{

	@Override
	public void run() {
		// TODO Auto-generated method stub	
		int b;
		for (int i = 0; i < size; i++) {
			try {
				b = queue.take();
				System.out.println(b);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
			
			
	}
		
}

方法六:使用信号量实现线程同步

信号量是用于在进程中发信号的整数值,Semaphore是一个计数信号量的实现类,信号量维护了一个许可集,通过aquire()&release()来获得和释放许可,其中许可数为1的信号量对于线程同步很有用。修改Transfer类,具体实现如下:

public class Transfer implements Runnable{
	private Bank bank;
	private Semaphore semaphore;
	public Transfer(Bank bank, Semaphore semaphore) {
		super();
		this.bank = bank;
		this.semaphore = semaphore;
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		for (int i = 0; i < 1000; i++) {
			try {
				semaphore.acquire();
				bank.deposit(10);
				System.out.println(bank.getAccount());
				semaphore.release();
			} catch (Exception e) {
				// TODO: handle exception
			}				
		}
	}		
}

方法七:使用原子变量实现线程同步

正因为绝大多数方法和操作都是非原子性的,才需要线程同步,如果我们能够把一系列的操作设置成原子性的,也就实现了同步。所谓原子性,即一系列的操作是一个整体,大家可以参考事务的概念,这一系列的操作要么不开始,一旦开始就直到执行结束。修改Bank类,具体实现如下:

public class Bank{
//	private int account = 100;
	private AtomicInteger account = new AtomicInteger(100);//初始化
		
	public void deposit(int money) {
		account.addAndGet(money);//addAndGet是原子操作
	}
	public int  getAccount() {
		return account.get();
	}
}


总共总结了七种实现方法,然而还有更多的方法在学习中。

Q&A

Q:为什么要实现同步?

A:没有同步的线程在实际生活中将会产生不可预计的后果,想象一下,如果转帐的行为不是原子性的,一个账户的增加和另一个账户的减少不能同步的话,破产的不是你就是银行。

Q:创建多个线程可以充分利用系统的资源,然而线程越多,线程之间竞争资源和切换也会增大开销,如何把握这个度?

A:对于有大量短生命周期线程的情况,我们可以通过使用线程池的方式来优化多线程。其原理就是,线程池中存在着多个处于可运行状态的线程,当向线程池中添加Runnable实现类实例的时候,其实是将任务提交到线程池本身的线程中执行。任务执行完毕之后,线程并不会终止,而是继续处于可运行的状态等待新的线程任务。









  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值