线程(二)join、yeild、同步(synchronized:同步块,同步方法;,Lock)、非线程安全单例模式、线程安全单例模式、多线程售卖电影票处理、通过线程依次打印A、B、C、wait和sleep
引起线程阻塞的方法:yeild、join、suspend、sleep、wait
join方法会让线程进入同步状态,会降低原来线程异步的效率(用的比较少),只能在方法已经调用以后才能够使用join方法
用了join方法以后会让每次执行都会执行完了以后才会第二条线程进入运行
System.out.println("程序开始运行"); //实例化继承了Thead线程 MyThread1 th1 = new MyThread1("线程A"); //开启线程 th1.start(); //实例化实现了Runnable接口的实例 MyThread2 runnable = new MyThread2(); //可以将Runnable实例封装至线程中 Thread th2 = new Thread(runnable); th2.start(); try { //将线程加入线程队列,使线程阻塞 //join方法必须在线程开启后才能调用
· //不管是谁的join方法在前面,都是1线程先运行,因为1线程先开始,加入join的线程会优先进行 th2.join(); th1.join(); } catch (InterruptedException e) { e.printStackTrace(); }
yeild方法
Java中实现线程同步的方式主要有两种:
1.synchronized同步处理
2.Lock锁实现同步
synchronized用于线程同步(为对象加对象锁),主要有2种使用方式:
1.对整个方法进行同步处理
2.对指定的对象进行同步(只能对不为null的对象才能同步,基本类型不能同步)
使用synchronized同步的对象,在执行结束后,会自动释放对象锁
线程同步:线程的对象在同一时间内,只允许一条线程访问;如果其他线程也需要访问该对象,则会进入阻塞状态,等待前一条线程访问结束后释放对象锁
1.非线程安全单例模式
public class User { private static User user; //构造方法 private User(){ } /** * 非线程安全的单例模式设计 * 获取单例 * @return */ public static User getInstance(){ if(user == null){ //使当前线程阻塞,暂缓线程的执行 Thread.yield(); user = new User(); } return user; } }
(单例模式通过线程调用,也会产生不同的哈希值,因为线程都是同时运行,可能在运行过程中多个线程运行创建单例对象时检测到的都是null,所以就会创建多个不同的用户对象;如果调用yeild方法是每个调用的对象都会在创建user对象的过程中暂缓,则本次创建时间会较长,更容易在本次线程没有创建对象成功的情况下下一个线程同时进入到新创建的过程,检测到的user还是为null,这样每一个线程都会重新创建一个新的user对象)
2.线程安全的单例模式(两种方法:同步方法,同步块)
会使有一条线程进入同步偶,其余线程都处于阻塞的状态,当这条线程进行完成时,下次进入到线程的顺序还是不确定的
同步方法的使用:
//同步方法 /** * 线程安全的处理设计 * 同步方法 * @return */ public synchronized static User getInstance(){ if(user == null){ //使当前线程阻塞,暂缓线程的执行 Thread.yield(); user = new User(); } return user; }
//同步块(只同步一部分,效率更高) /** *线程安全的处理设计
*同步块 * @return */ public static User getInstance(){ //使用同步对象(同步块),仅针对于同步块的内容进行锁定 //不能对为null的对象进行同步 synchronized (user){ } if(user == null){ //使当前线程阻塞,暂缓线程的执行 Thread.yield(); user = new User(); } return user; }
通过线程run方法对User进行实例化,进行调用单例模式
public class UserThread extends Thread { //同过线程run方法对User进行实例化 @Override public void run() { User user = User.getInstance(); //获取对象的哈希值,判断是否是同一个对象 System.out.println(user.hashCode()); } }
通过循环多次通过启动线程来调用单例
public class Test { public static void main(String[] args) { for(int i = 1; i <= 10; i++){ //使用线程创建对象 UserThread t = new UserThread(); t.start(); } } }
同步块的应用:
若是在线程内部进行同步,且同步的对象是每个线程自有的,那么同步没有任何意义,因为没有产生并发访问问题
public class DemoThread extends Thread{ //对象都是每个线程自己拥有的单独new出来的 private Object obj=new Object(); @Override public void run() { //同步的对象,必须是在多个线程中并发访问的同一个对象 //此时同步没有任何意义 synchronized (obj){ } } }
若是想要同步有意义,则obj必须有外部传入,多个线程都能访问到这个obj,此时才会产生并发访问,同步才有意义
设置一个用于外部传入对象的类
/** *要传入的对象可以设置成一个类 */ public class LockObject { //没有赋值,则默认值是0 private int n; public int getN() { return n; } public void setN(int n) { this.n = n; } }
创建线程
public class DemoThread extends Thread{ //设置属性 private LockObject obj; //通过构造方法给属性赋一个外部传入的值 public DemoThread(String name,LockObject obj){ //获取线程名称 super(name); this.obj = obj; } @Override public void run() { //同步的对象,必须是在多个线程中并发访问的同一个对象 synchronized (obj){ for(int i = 1; i <= 10; i++){ //计算累计的1-10的和 obj.setN(obj.getN()+i); }
//打印也要在同步内,不然打印的时候也会争先后,可能会出现两次打印是相同的结果 System.out.println("线程"+getName()+"的计算结果:"+obj.getN()); } } }
通过循环调用线程
public class Test { public static void main(String[] args) { //创建多条线程需要并发访问的对象 LockObject obj = new LockObject(); for(int i = 1; i <= 10; i++){
//传入名称和外部需要同步的对象 DemoThread thread = new DemoThread(i+"",obj); thread.start(); } } }
例子:使用ArrayList<Integer> 创建10条线程,来累加1-10的和,要求使用线程同步的方式将计算的和写入ArrayList的第一个元素的位置
创建线程(同步块)
import java.util.ArrayList; public class ListThread extends Thread { //创建需要外部传入的对象 private static ArrayList<Integer> list; //通过构造方法将对象赋一个外部闯入的值 public ListThread(ArrayList<Integer> list) { this.list = list; } int sum=0; /** * 重写run方法 */ @Override public void run() { //将传入的list保护起来,只能一个线程访问 synchronized (list){ //计算1-10的和 for(int i=1;i<=10;i++){ sum+=i; } //计算每个需要写入集合中是数(1-10的和加上集合中上一个数) if(list.size()-1>=0){ sum=sum+list.get(list.size()-1); } //将计算好的数添加至集合中 list.add(sum); System.out.println("此时值为:"+sum); } } }
测试类
import java.util.ArrayList; public class Test { public static void main(String[] args) { //创建外部集合 ArrayList<Integer> list=new ArrayList<>(); for(int i=1;i<=10;i++){ //将集合传入,这样是这个集合每个线程都能同时访问到,能够出现线程问题 ListThread th=new ListThread(list); th.start(); } } }
Lock锁实现同步:
加上线程锁
//创建线程锁对象
ReentrantLock lock = new ReentrantLock();
lock.lock();
for(){
//需要加锁的内容
}
lock.unlock();
设置一个用于外部传入对象的类
public class LockObject { private int n; public int getN() { return n; } public void setN(int n) { this.n = n; } }
创建线程(线程锁)
public class DemoThread extends Thread{ private LockObject obj; //创建线程锁对象 ReentrantLock lock = new ReentrantLock(); public DemoThread(String name, LockObject obj){ super(name); this.obj = obj; } @Override public void run() { //加上线程锁 lock.lock(); for(int i = 1; i <= 10; i++){ obj.setN(obj.getN()+i); } System.out.println("线程"+getName()+"的计算结果:"+obj.getN()); //解锁 lock.unlock(); } }
测试类
public class Test { public static void main(String[] args) { //创建多条线程需要并发访问的对象 LockObject obj = new LockObject(); for(int i = 1; i <= 10; i++){ DemoThread thread = new DemoThread(i+"",obj); thread.start(); } } }
例子:实现多线程售卖电影票处理
一共100张票,设置3个售票站,每个售票站都具备售票功能,打印显示每个售票点售卖的电影票情况
思路:每个售票点都是同样的工作,所以创建一个线程类实例化三次即可
电影票对象
/** * 票对象 */ public class TickObject { //票的总数 private int count = 100; public int getCount() { return count; } public void setCount(int count) { this.count = count; } }
创建线程类
public class TickThread extends Thread{ //设置需要外部传入的对象 private TickObject tick; //通过外部传入给属性赋一个值 public TickThread(String name ,TickObject tick){ super(name); this.tick = tick; } @Override public void run() { //当影票还未售完时反复售卖影票 while(tick.getCount() > 0){ //同步票对象,要放在while内部,不然在外部同步其中一个线程进入到方法中以后,会循环卖掉所有的票 synchronized (tick){ //进入同步以后,还要再次判断是否还有票,因为还剩最后一张票的时候可能三个线程都进入到while循环中,
但是只有一个线程进入同步卖掉了最后一张票,剩余两张就会多卖两张没有的票 if(tick.getCount() > 0){ //每次售卖一张影票,票的总数将会减少 tick.setCount(tick.getCount()-1); System.out.println("售票站"+getName()+"卖出了第"+(100-tick.getCount())); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } } }
测试类
public class Test { public static void main(String[] args) {
//创建一个用于外部传入的电影票对象 TickObject tick = new TickObject();
//实例化三条线程,代表三个售票站 TickThread t1 = new TickThread("A",tick); TickThread t2 = new TickThread("B", tick); TickThread t3 = new TickThread("C", tick); t1.start(); t2.start(); t3.start(); } }
wait方法阻塞线程(必须要在线程同步的时候才能用wait方法,专门用于多线程并发阻塞,所有必须要再同步内使用wait)
可以通过 “ 对象.notify(); ” 或者 “ 对象.notifyAll(); ” 进行恢复
线程1
public class DemoThead1 extends Thread{ private Object obj; public DemoThead1(Object obj) { this.obj = obj; } @Override public void run() { //wait方法使用时,使用wait的对象必须要进行同步处理 synchronized (obj){ try { System.out.println("暂停线程A"); //将调用该对象的线程进行阻塞 obj.wait();
//wait后的内容在没有notify前是不会打印出来的,因为线程已经阻塞了,
只能在另外一个访问同一对象的线程中将其恢复 System.out.println("恢复暂停的线程A"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
线程2
public class DemoThread2 extends Thread{ private Object obj; public DemoThread2(Object obj) { this.obj = obj; } @Override public void run() { synchronized (obj){ System.out.println("准备恢复A线程");
//此处1线程已经恢复了,但是obj在2线程的锁定中,所以此处会先将2线程走完释放掉对象锁,才能开始运行1线程 obj.notify(); System.out.println("B线程执行结束"); } } }
测试类
public class Test { public static void main(String[] args) { Object obj = new Object(); DemoThead1 t1 = new DemoThead1(obj); DemoThread2 t2 = new DemoThread2(obj); t1.start(); t2.start(); try {
//此处加入队列是为了不让2线程跑到1线程前面运行 t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } } }
例子:通过线程依次打印A、B、C
每条线程负责打印一个字母,打印显示3次ABC循环,最终显示为 ABCABCABC
思路:通过阻塞控制线程的优先级,要使用wait更加可控顺序,因为线程的阻塞和恢复没法指定,所以通过打印的内容来控制进入同步的条件
创建一个记录打印内容的类
/** * 打印对象 */ public class TypeObj { //记录第一次进入同步的条件 private String targetWord = "A"; public String getTargetWord() { return targetWord; } public void setTargetWord(String word) { this.targetWord = word; } }
创建线程
/** * 用于打印字母的线程 */ public class TypeThread extends Thread{ private String word; private TypeObj obj; public TypeThread(String word,TypeObj obj) { this.word = word; this.obj = obj; } @Override public void run() {
//记录打印的次数 int count = 0;
//每个线程循环打印三次 while(count < 3){ //同步打印对象 synchronized (obj){ //通过对比该线程需要打印的字母和打印对象中的打印值,判断当前线程是否是需要打印字母的线程 if(this.word.equals(obj.getTargetWord())){
//如果是需要打印字母的线程 //累计打印的次数 count++;//打印字母 System.out.println(word); //修改打印对象中的需要打印的字母 if(obj.getTargetWord().equals("A")){ obj.setTargetWord("B"); } else if(obj.getTargetWord().equals("B")){ obj.setTargetWord("C"); } else{ obj.setTargetWord("A"); } //唤醒其他处于阻塞状态的线程 obj.notifyAll(); } //如果当前线程不是需要打印的线程,则将其进入阻塞 else{ try { //阻塞当前线程 obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } }
测试类
public class Test { public static void main(String[] args) {
//创建用于传入的打印对象 TypeObj obj = new TypeObj();
//创建三个打印不同字母的线程 TypeThread t1 = new TypeThread("A",obj); TypeThread t2 = new TypeThread("B", obj); TypeThread t3 = new TypeThread("C", obj); //启动线程 t1.start(); t2.start(); t3.start(); } }
wait和sleep的区别点:
1.sleep的阻塞状态是timed_waitting,时间到达后将自动恢复至运行时装填;wait的阻塞状态时waitting状态,必须通过notify或者notifyAll进行恢复
2.sleep只能对当前线程进行阻塞;wait方法是多线程并发情况下进行阻塞,允许跨线程调用
3.sleep休眠不释放对象锁;wait释放对象锁(阻塞过程中,其他线程可以访问同步对象)