博文正文开头格式:(2分)
项目 |
内容 |
这个作业属于哪个课程 |
https://www.cnblogs.com/nwnu-daizh/ |
这个作业的要求在哪里 |
https://www.cnblogs.com/nwnu-daizh/p/12073034.html |
作业学习目标 |
(1) 理解和掌握线程的优先级属性及调度方法; (2) 掌握线程同步的概念及实现技术; (3) Java线程综合编程练习 |
随笔博文正文内容包括:
第一部分:总结线程同步技术(10分)
一.线程安全、线程同步、线程互斥、线程通信
线程安全:是指多线程执行同一份代码每次执行结果都和单线程一样。
线程同步:对临界区的共享数据,A线程去操作数据,并且需要另一线程B的操作才能继续完成,这种线程之间协作的就是线程同步。
线程互斥:对临界区的共享数据,两个线程都有修改情况,如果没有加锁或cas等的操作会造成数据混乱异常,这种就是线程互斥。
线程通信:可以认为是线程同步的扩展,因为wait/notify必须获取了对象锁才能使用,通过wait/notify这种方式实现两个线程的等待唤醒。
二.线程通信和进程通信的几种方式
线程通信:共享变量、wait/notify、lock/ Condition(Condition通过ReentrantLock的new condition,其await等同于wait,signal等同于notify)。
进程通信:内存映射、消息队列、socket。
线程同步的7种方式
synchronized同步方法。如果是普通方法会锁住这个对象,如果是静态方法锁住的是整个类。
synchronized同步代码块。
volatile修饰变量。
重入锁ReenreantLock。实现了Lock接口,可重入,但效率低。
ThreadLocal线程级别的变量共享。
阻塞队列LinkedBlockingQueue。主要通过synchronized的put、take实现。
原子变量。
Runnable、Thread、Callable、Future、FutureTask、ThreadLocal
Runnable:是一个接口,只有一个run方法,当线程进入running状态,就会执行run方法,可以认为是对运行任务的封装。
Thread:是一个类,当线程进入running状态,会执行run方法,如果没有重写run方法默认会调用Runnable的run方法。
Callable:是一个接口,只有一个call方法,与runnable相似,但不同的是有返回值并能抛出异常。
Future:是一个接口。是对runnable、callable的运行结果的操作,可以判断运行是否完成、是否取消以及获取运行的结果,获取运行结果调用get方法,这种方式获取结果是同步的。
FutureTask:是一个实现类,实现了Future和Runnable接口。所以既可以作为Runnable去启动一个线程,也可以作为Future去获取线程运行结果。而需要主要的是,Future是接口不能直接操作运行结果,FutureTask可以,也是Future唯一的实现类。
ThreadLocal:内部是一个map,其作用是将一个变量限定为线程级的变量。
获取线程运行结果:
使用callable的call方法可以有返回值,一般搭配ExectorService的submit(callable)方法使用,其返回值是Future,再通过Future的get方法获取线程运行结果。类似的可以将Future用FutureTask代替。
Executor框架:
Executor框架是一个异步执行框架,执行任务的线程相当于消费者,提交任务的线程相当于生产者。主要API包括以下:
Executor:执行器接口,只有一个execute(Runnable)的方法,可以执行一个Runnable的任务,但是该方法没有返回值,使用较少。
ExecutorService:是Executor的子接口,也是ThreadPoolExecutor的父接口,增加了一些方法包括关闭等,最重要的是增加了submit(Callable)的方法,返回值是Future<T>或FutureTask<T>。
ScheduledExecutorService:是ExecutorService的子接口,主要增加了一些定时任务的方法。
ThreadPoolExecutor:JDK的线程池类。
Executors:是一个工厂类,提供了5个静态方法去创建线程池。
竞态条件、临界区
竞态条件是指程序的运行结果取决于线程执行的顺序,也就是程序结果对不对看运气。最典型的竞态条件是先检查后执行,也就是前面的检查是一个无效的过期的检查,但是执行了后面的代码。
临界区是指访问那种一次只能有一个线程执行的资源的代码块。
Synchronized、Lock、ReentrantLock、ReadWriteLock 、ReentrantReadWriteLock
Synchronized是JVM内置的关键字。可以使用在代码块、方法上;非公平锁;可重入锁;不能响应中断;在执行完了锁住的代码块或发生中断会自动释放锁;无法得到是否持有锁、是否有等待线程等信息。
Lock是一个接口,ReentrantLock是Lock的实现类,提供了lock、trylock等获取锁的方法和unlock释放锁的方法;默认是非公平锁。也可以设置为公平锁;可重入锁;能响应中断;需要主动释放锁,否则很有可能发生死锁现象;能得到锁的一些信息。
ReadWriteLock是另外一个接口,ReentrantReadWriteLock是ReadWriteLock的实现类,主要特点是有readLock和writeLock两种锁,也需要主动释放锁。如果一个线程占了读锁,不影响其他线程获取读锁,但若其他线程要获取写锁,需要等待读锁完成;如果一个线程占了写锁,那么其他线程获取读写锁都需要等待该写锁释放。
一些锁的定义
可重入锁:可重入锁是指当持有锁的线程再次获取这个锁时能立即成功,锁的计数+1,当执行完代码块时计数-1,当计数为0时释放锁;不可重入锁是持有锁的线程再次获取时会造成死锁的情况。
可中断锁:可中断锁就是线程在等待获取锁的时候可以中断,去处理别的任务。Synchronized不是可中断锁,lock就是可中断锁。
(非)公平锁:公平锁是指获取锁是有顺序的,例如ReentrantLock可以通过构造方法设置成公平锁,等待时间长的锁优先获取锁;非公平锁就是获取锁跟顺序无关,随机选择一个线程分配锁,例如synchronized就是典型的非公平锁,这种非公平锁有可能导致某个线程一直获取不到锁。
独享锁共享锁(互斥锁读写锁):独享锁是指这个锁同一时间只能被一个线程持有,例如synchronized、ReentrantLock;共享锁是这个锁可以被多个线程共同持有,例如ReadWriteLock和其子类ReentrantReadWriteLock,其读锁是共享锁,写锁是独享锁。而互斥锁和读写锁就分别是独享锁和共享锁的具体表现。
乐观锁悲观锁:乐观锁是认为读多写少,所以在读的时候不会加锁,在写的时候会先照常执行,当发现执行结果不对时会舍弃本次操作再重试,例如CAS算法;悲观锁是认为读少写多,所以在每次读写都会进行加锁,例如独占锁。
偏向锁、轻量级锁、重量级锁:这三种其实是锁的三种状态,并且都是真的synchroinzed而言的。偏向锁是指同步代码一直被一个线程访问,那么这个线程会自动获取这个锁,这个锁就是偏向锁;轻量级锁是指之前的偏向锁对应的代码块被另一个线程访问了,那么这个偏向锁会升级为轻量级锁,这时线程获取锁通过自旋的方式获取,减少上下文切换的消耗;重量级锁是指线程去获取轻量级锁的自旋了一段时间还是不能获取到锁,为了降低CPU的消耗,会让该锁升级为重量级锁,线程获取重量级锁的时候时候进入阻塞状态。
CAS算法
CAS(Compareand Swap,比较并交换)是乐观锁的一种典型算法实现。其核心是对于修改操作,会有旧值、预期值和新值,当去修改内存中的旧值时,会先去判断是否和自己的预期值相等,如果相等说明没有被别的线程修改过,直接替换为新值;如果不相等说明被别的线程修改了,就舍弃本次操作。这种方式能优化锁,提高效率,但是也可能出现ABA(A值被改为B,有改为了A,CAS不能发现)的情况。
锁优化的6种方式
(减少锁的持有时间)细化锁。减少加锁的代码块,因为加锁部分的代码越长运行时间越长,别的线程等待时间越长。
(减小锁的粒度)分段锁。例如concurrentHashMap,内部实现是多个segment(reentrantLock子类),通过将这个表分段提升性能。
粗化锁。对于重量级锁会是等待线程进入阻塞状态,增加线程上下文切换的开销,因此频繁的使用锁还不如使用较粗粒度的锁,虽然单个锁的运行时间长了,但是减少了CPU开销。
自旋锁。针对等待时间短的锁。
锁分离。例如读写锁ReadWriteLock,或LinkedBlockingQueue维护头尾两个锁。
锁消除。JIT会在编译时对不会共享的对象进行锁消除。
synchronized关键字
synchronized关键字作用:
某个类内方法用synchronized 修饰后,该方法被称为同步方法;
只要某个线程正在访问同步方法,其他线程欲要访问同步方法就被阻塞,直至线程从同步方法返回前唤醒被阻塞线程,其他线程方可能进入同步方法。
一个线程在使用的同步方法中时,可能根据问题的需要,必须使用wait()方法使本线程等待,暂时让出CPU的使用权,并允许其它线程使用这个同步方法。
线程如果用完同步方法,应当执行notifyAll()方 法通知所有由于使用这个同步方法而处于等待的 线程结束等待。
测试程序1:
l 在Elipse环境下调试教材651页程序14-7,结合程序运行结果理解程序;
代码如下:
package synch; /** * 这个程序展示了多线程如何安全地访问数据结构。 * @version 1.31 2015-06-21 * @author Cay Horstmann */ public class SynchBankTest { public static final int NACCOUNTS = 100; public static final double INITIAL_BALANCE = 1000; public static final double MAX_AMOUNT = 1000; public static final int DELAY = 10; public static void main(String[] args) { Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE); for (int i = 0; i < NACCOUNTS; i++) { int fromAccount = i; //可以用InterruptedException捕捉异常 Runnable r = () -> { try { while (true) { int toAccount = (int) (bank.size() * Math.random()); double amount = MAX_AMOUNT * Math.random(); bank.transfer(fromAccount, toAccount, amount); Thread.sleep((int) (DELAY * Math.random())); } } catch (InterruptedException e) { } }; //runnable类的run方法,不断地从一个固定的银行账户取出钱款 //在每一次迭代中,run方法随机选择一个目标账户和一个随机账户,调用bank对象和transfer方法,然后睡眠 Thread t = new Thread(r); t.start(); } } }
package synch; import java.util.*; import java.util.concurrent.locks.*; /** *拥有多个银行帐户的银行使用锁来序列化访问。 * @version 1.30 2004-08-01 * @author Cay Horstmann */ public class Bank //用一个锁来保护Bank类的transfer方法; { private final double[] accounts; private Lock bankLock; private Condition sufficientFunds; /** * Constructs the bank. * @ param n帐户数量 * @参数初始平衡每个帐户的初始余额 */ public Bank(int n, double initialBalance) { accounts = new double[n]; Arrays.fill(accounts, initialBalance); bankLock = new ReentrantLock(); //ReentrantLock构建一个带有公平策略的锁 sufficientFunds = bankLock.newCondition(); //用newCondition方法获得有个条件对象 } /** *将资金从一个账户转移到另一个账户。 * @要转帐的帐户参数 * @要转移到的帐户参数 * @ param金额要转移的金额 */ public void transfer(int from, int to, double amount) throws InterruptedException { //通过使用锁爱保护检查与转账动作来做到这一点 bankLock.lock(); try { while (accounts[from] < amount) sufficientFunds.await(); //余额不足,调用sufficientFunds.await()让当前线程阻塞 System.out.print(Thread.currentThread()); accounts[from] -= amount; System.out.printf(" %10.2f from %d to %d", amount, from, to); accounts[to] += amount; //两个线程同时执行指令 System.out.printf(" Total Balance: %10.2f%n", getTotalBalance()); sufficientFunds.signalAll(); } finally { bankLock.unlock(); } } /** *获取所有帐户余额的总和。 * @返回总余额 */ public double getTotalBalance() { bankLock.lock(); //用ReentrantLock保护代码块的基本结构 try { double sum = 0; for (double a : accounts) sum += a; return sum; } finally { bankLock.unlock(); //make sure the lock is unlocked even if an exception is thrown } } /** *获取银行账户的数量。 * @返回账号 */ public int size() { return accounts.length; } }
运行结果:
锁对象:
如果两个线程试图放问同一个Bank对象(每个Bank都有一个锁),那么锁将以串行的方式提供服务;但是,如果两个线程访问不同的Bank对象,每一个线程将得到不同的所对象,两个线程都不会发生阻塞。
锁是可重入的,因为线程可以重复得获得已经持有的锁。锁保持一个持有计数来跟踪对lock方法的嵌套使用。线程每一次调用lock方法都会使计数增加,每次调用Unlock都会使计数减少,由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。
举个例子,在我们最终的代码中,transfer方法调用getTotalBalance方法,这也会封锁bankLock对象,此时bankLock对象持有的锁的计数为2。当getTotalBalance方法退出的时候,持有计数变回1。当transfer方法退出时,持有计数变为0,线程释放锁。
条件对象
对程序上了锁,这样避免了一些令我们难过的情况,不过事实上,事情比我们往往比我们想的还要复杂,现在设想:银行的一个线程,进入了临界区,他想进行取钱操作,但是发现没有足够的钱可以取出(可能是工资还没打到用户的账户上),只有钱足够时,才能去出钱,但是他现在又对本身的Bank对象上了锁,因此别的线程无权对这个Bank对象进行操作,这时,就需要使用条件对象。
测试程序2:
在Elipse环境下调试教材655页程序14-8,结合程序运行结果理解程序;
代码如下:
package synch2; import synch.Bank; /** * 这个程序展示了多线程如何安全地访问数据结构。 * @version 1.31 2015-06-21 * @author Cay Horstmann */ public class SynchBankTest2 { public static final int NACCOUNTS = 100; public static final double INITIAL_BALANCE = 1000; public static final double MAX_AMOUNT = 1000; public static final int DELAY = 10; public static void main(String[] args) { Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE); for (int i = 0; i < NACCOUNTS; i++) { int fromAccount = i; //可以用InterruptedException捕捉异常 Runnable r = () -> { try { while (true) { int toAccount = (int) (bank.size() * Math.random()); double amount = MAX_AMOUNT * Math.random(); bank.transfer(fromAccount, toAccount, amount); Thread.sleep((int) (DELAY * Math.random())); } } catch (InterruptedException e) { } }; //runnable类的run方法,不断地从一个固定的银行账户取出钱款 //在每一次迭代中,run方法随机选择一个目标账户和一个随机账户,调用bank对象和transfer方法,然后睡眠 Thread t = new Thread(r); t.start(); } } }
package synch2; import java.util.*; /** *具有多个使用同步原语的银行帐户的银行 * @version 1.30 2004-08-01 * @author Cay Horstmann */ public class Bank { private final double[] accounts; /** * Constructs the bank. * @param n the number of accounts * @param 参数初始平衡每个帐户的初始余额 */ public Bank(int n, double initialBalance) { accounts = new double[n]; Arrays.fill(accounts, initialBalance); } /** *将资金从一个账户转移到另一个账户。 * @要转帐的帐户参数 * @要转移到的帐户参数 * @ param金额要转移的金额 */ public synchronized void transfer(int from, int to, double amount) throws InterruptedException { //每一个对象有一个内部锁,并且该锁有一个内部条件 //由锁来管理那些师徒进入synchronized方法的线程,由条件来管理那些调用wait 的线程 while (accounts[from] < amount) wait(); System.out.print(Thread.currentThread()); accounts[from] -= amount; System.out.printf(" %10.2f from %d to %d", amount, from, to); accounts[to] += amount; System.out.printf(" Total Balance: %10.2f%n", getTotalBalance()); notifyAll(); } /** *获取所有帐户余额的总和。 * @return the total balance */ public synchronized double getTotalBalance() { double sum = 0; for (double a : accounts) sum += a;//获取总和 return sum; } /** *获取银行账户的数量。 * @return the number of accounts */ public int size() { return accounts.length; } }
运行截图:
Synchronized
单从语法的角度看,要实现线程的同步,或者是解决共享资源的竞争问题很简单,就是在冲突发生时,当资源被一个任务(Thread)使用时,在其上加上锁即可。这个加锁有JVM完成,不需要我们关心,使用Synchronized表示要对这个资源加上锁。就实际开发来说,这个“资源”是我们的方法或者方法内的一块儿代码。
Synchronized使用的是互斥量机制实现的资源加锁,基本上所有的并发模式在解决线程冲突问题的时候,都是采用的序列化共享资源的方案。这意味着在某一时刻只允许一个任务访问共享资源。通常这都是在代码前加上一条锁语句来实现的,这就使得在一段时间内只有一个任务可以运行这段代码。
或者换个角度,从OS角度看,我们的线程竞争的是对同一块而内存的读写操作,也就是内存资源。而这块儿内存资源在Java中被表现为一个变量、一个对象。当然共享的资源也可以是文件、I/O、打印机等。所有要控制共享资源,得先把它包装进一个对象,然后把所有要访问这个资源的方法标记为Synchronized。而这个对象将被JVM自动赋予一把锁,这就是对象锁。
测试程序3:
l 在Elipse环境下运行以下程序,结合程序运行结果分析程序存在问题;
代码如下:
class Cbank { private static int s=2000; public static void sub(int m) { int temp=s; temp=temp-m; try { Thread.sleep((int)(1000*Math.random())); } catch (InterruptedException e) { } s=temp; //用InterruptedException捕获异常 System.out.println("s="+s); } } class Customer extends Thread { public void run() { for( int i=1; i<=4; i++) Cbank.sub(100); } } public class Thread3 { public static void main(String args[]) { Customer customer1 = new Customer(); Customer customer2 = new Customer(); customer1.start(); customer2.start();//调用Thread类中的start方法开启线程 } }
运行截图:
解决后代码:
package Test; class Cbank { private static int s=2000; public synchronized static void sub(int m)//用synchronized来解决冲突问题 { int temp=s; temp=temp-m; try { Thread.sleep((int)(1000*Math.random())); } catch (InterruptedException e) { } s=temp; System.out.println("s="+s); } //异常处理 } class Customer extends Thread { public void run() { for( int i=1; i<=4; i++) Cbank.sub(100); } } public class Thread3 { public static void main(String args[]) { Customer customer1 = new Customer(); Customer customer2 = new Customer(); customer1.start(); customer2.start(); } }
运行截图:
程序在运行时有冲突发生,所以利用synchronized
使得只要某个线程正在访问同步方法,其他线程欲要访问同步方法就被阻塞,直至线程从同步方法返回前唤醒被阻塞线程,其他线程方可能进入同步方法。
实验2 编程练习
利用多线程及同步方法,编写一个程序模拟火车票售票系统,共3个窗口,卖10张票,程序输出结果类似(程序输出不唯一,可以是其他类似结果)。
Thread-0窗口售:第1张票
Thread-0窗口售:第2张票
Thread-1窗口售:第3张票
Thread-2窗口售:第4张票
Thread-2窗口售:第5张票
Thread-1窗口售:第6张票
Thread-0窗口售:第7张票
Thread-2窗口售:第8张票
Thread-1窗口售:第9张票
Thread-0窗口售:第10张票
实验2:结对编程练习包含以下4部分(10分)
1) 程序设计思路简述;
程序的主要设计思路没有太繁琐,只是新建三个售票口,在thread类中创建线程并开启线程,然后在run方法中定义线程任务,进行异常处理后设计在10张票数内时将售票情况进行打印,票数超过10张时程序结束;
2) 符合编程规范的程序代码;
package Test; public class Ticket { public static void main(String[] args) { Mythread mythread = new Mythread(); Thread ticket1 = new Thread(mythread); Thread ticket2 = new Thread(mythread); Thread ticket3 = new Thread(mythread);//新建三个Thread类对象 ticket1.start(); ticket2.start(); ticket3.start();//调用thread类的start方法来开启线程 } } class Mythread implements Runnable {//实现runnable接口进行线程的创建 int ticket = 1; boolean flag = true; @Override public void run() { //将线程任务代码定义到run方法中 while (flag) { try { Thread.sleep(500); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); //异常处理 } synchronized (this) { if (ticket <= 10) { System.out.println(Thread.currentThread().getName() + "窗口售:第" + ticket + "张票");//获取线程名称 ticket++; //票数在10张之内时,进行打印直到售出10张票时停止 } if (ticket > 10) { flag = false; } } } } }
3) 程序运行功能界面截图:
实验总结:(5分)
本周学习了有关线程的剩余的知识点,通过老师上课的讲解以及对课本示例代码的编辑和运行,掌握了线程同步的概念及实现技术,面对多线程并发运行不确定性问题解决方案:需要引入线 程同步机制,使得另一线程要使用该方法,就只 能等待。线程同步主要是解决多线程并发运行不确定性问题。还在示例代码中学习了ReentrantLock类,在共享内存的类方法前加synchronized修饰符的方法,对synchronized有了一定的了解,通过最后的Java线程综合编程练习对这些知识点有了更清晰的认识,所以在之后还得在课后多加练习和复习,才能有一定的记忆基础。