java8--多线程(java疯狂讲义3复习笔记)
多线程这块,平时用的框架里都封装好了,只有写批处理和工具包时用过几次.现在水平仅仅限于会用的程度,需要全面深入学习多线程.
主要内容:创建线程,启动线程,控制线程,多线程的同步,线程池,使用线程安全的集合类
16.1.1 线程和进程
线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程.线程可以拥有自己的堆栈,程序计数器和局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源.因为多个线程共享父进程的全部资源,因此编程更加方便,带也需要更加小心.
16.2 线程的创建和启动
继承Thread类创建线程类
public class FirstThread extends Thread{ private int i; @Override public void run(){ for(i=0;i<100;i++){ System.out.println(getName() + " " +i); } } public static void main(String[] args){ FirstThread xx = new FirstThread(); xx.setName("xx"); xx.start(); for(int i=0;i<100;i++){ System.out.println(Thread.currentThread().getName()+"------ " +i); if(i==20){ new FirstThread().start(); new FirstThread().start(); } } } }
实现Runnable接口创建线程类
public class RunnableThread implements Runnable{ private int i;
@Override public void run(){ for (;i<100;i++){ System.out.println(Thread.currentThread().getName()+"wwwwwwww "+i); } } public static void main(String[] args){ for (int i = 0;i<100;i++){ // System.out.println(Thread.currentThread().getName()+" "+i); if(i==20){ RunnableThread st = new RunnableThread(); new Thread(st,"新线程1").start(); new Thread(st,"新线程2").start(); } } } }
这两种方法的区别:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体.而实际的线程对象依然是Thread实例,只是改Thread线程负责执行其target的run()方法.
第一个例子里面每个thread各操作一个对象,所以打印的i是互不相干的(各自打印100个数)
第二个例子里面两个thread操作的是同一个对象,所以打印的i是相互关联的(一共打印了100个数).
有返回值的线程方法
public class CallableThread { public static void main(String[] atgs){ // CallableThread rt = new CallableThread(); FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{ int i= 0; for(;i<100;i++){ System.out.println(Thread.currentThread().getName()+" 1循环变量i的值:"+i); } return i; }); for(int i = 0;i<100;i++){ System.out.println(Thread.currentThread().getName()+" 2循环变量i的值:"+i); if (i==20){ new Thread(task,"有返回值的线程").start(); } } try{ System.out.println("子线程的返回值:"+task.get()); }catch(Exception ex) { ex.printStackTrace(); } } }
线程的生命周期
有新建(new),就绪(Runnable),运行(Running),阻塞(Blocked)和死亡(Dead)5种状态.
join()和join(long millis),等待join的线程执行完了,被join的线程继续执行
public class FirstThread extends Thread{ private int i; @Override public void run(){ for(i=0;i<100;i++){ System.out.println(getName() + " " +i); } } public static void main(String[] args) throws InterruptedException{ FirstThread xx = new FirstThread(); xx.setName("xx"); xx.start(); xx.setPriority(MIN_PRIORITY); for(int i=0;i<100;i++){ System.out.println(Thread.currentThread().getName()+"------ " +i); if(i==20){ FirstThread thread1 = new FirstThread(); thread1.start(); thread1.setPriority(MAX_PRIORITY); FirstThread thread2 = new FirstThread(); thread2.start(); thread2.join(); new FirstThread().start(); } } } }
调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程.
要将某个线程设置为后台线程,必须在start()之前调用
线程睡眠sleep(long millis)
sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行.
16.4.5 改变线程的优先级
Thread类提供了setPriority(int newPriority)和getPriority()来设置
和返回指定线程的优先级.10最大,1最小,正常为5
16.5 线程同步
1.synchronized(obj){
}
2.synchronized修饰某个方法或代码块,但不能修饰构造器和成员变量
重点解析:http://www.cnblogs.com/GnagWang/archive/2011/02/27/1966606.html
Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
四、第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
五、以上规则对其它对象锁同样适用.
延伸:单线程环境下应该使用StringBuilder来保证较好的性能,当需啊哟保证多线程安全时,
就 应该使用StringBuffer
16.5.5 同步锁(Lock)
Lock是控制多个线程对共享资源进行访问的工具.通常,锁提供了对共享资源的独占访问,每次只能有一个
线程对Lock对象加锁,线程开始访问共享资源之前,应该先获得Lock对象.
//定义锁对象 private final ReentrantLock lock = new ReentrantLock(); //定义需要保证线程安全的方法 public void m(){ lock.lock(); try{ //需要保证线程安全的代码 //...method body } //使用finally块来保证释放锁 finally{ lock.unlock(); } }
ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁
ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显示调用unlock()来释放锁,所以一段被锁包不户的代码可以调用另一个被相同锁保护的方法.
死锁
package learnThread; class A { public synchronized void foo( B b ) { System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了A实例的foo()方法" ); // ① try { Thread.sleep(200); } catch (InterruptedException ex) { ex.printStackTrace(); } System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用B实例的last()方法"); // ③ b.last(); } public synchronized void last() { System.out.println("进入了A类的last()方法内部"); } } class B { public synchronized void bar( A a ) { System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了B实例的bar()方法" ); // ② try { Thread.sleep(200); } catch (InterruptedException ex) { ex.printStackTrace(); } System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用A实例的last()方法"); // ④ a.last(); } public synchronized void last() { System.out.println("进入了B类的last()方法内部"); } } public class DeadLock implements Runnable { A a = new A(); B b = new B(); public void init() { Thread.currentThread().setName("主线程"); // 调用a对象的foo方法 a.foo(b); System.out.println("进入了主线程之后"); } public void run() { Thread.currentThread().setName("副线程"); // 调用b对象的bar方法 b.bar(a); System.out.println("进入了副线程之后"); } public static void main(String[] args) { DeadLock dl = new DeadLock(); // 以dl为target启动新线程 new Thread(dl).start(); // 调用init()方法 dl.init(); } } ----------------------------------------------------------------------- 当前线程名: 主线程 进入了A实例的foo()方法 当前线程名: 副线程 进入了B实例的bar()方法 当前线程名: 副线程 企图调用A实例的last()方法 当前线程名: 主线程 企图调用B实例的last()方法
16.6 线程通信
包括
16.6.1传统的线程通信,用this或者是被锁的对象来调用.
wait(),notify(), notifyAll()
16.6.2使用Condition控制线程通信,调用Lock对象的newCondition()方法即可.
await(),signal(),signalAll()
16.6.3 使用阻塞队列(BlockingQueue)控制线程通信
BlockingQueue是Queue的子接口,特征为:当生产者线程试图向BlockingQueue中放入元素时,如果该对垒已满,
则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞.
put(E e)
take(E e)
16.7 线程组合未处理的异常
16.8线程池
当程序中需要创建大量生存期很短暂的线程时,应该考虑使用线程池
与线程连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个线程来执行他们的run()或call方法,当run()或call()方法执行结束后,该线程并不会立即死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run()或call()方法.
步骤如下:
1.调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池
2.创建Runnable实现类或Callable实现类的实例,作为线程执行任务.
3.调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例.
4.当不想提交任何任务时,调用ExecutorService对象的shutdown()方法来关闭线程池.
public static void threadPoolTest(){ // 创建足够的线程来支持4个CPU并行的线程池 // 创建一个具有固定线程数(6)的线程池 ExecutorService pool = Executors.newFixedThreadPool(4); // 使用Lambda表达式创建Runnable对象 Runnable target = () -> { for (int i = 0; i < 1 ; i++ ) { System.out.println(Thread.currentThread().getName() + "的i值为:" + i); } }; // 向线程池中提交两个线程 pool.submit(target); pool.submit(target); pool.submit(target); pool.submit(target); try { Thread.sleep(10000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } pool.submit(target); pool.submit(target); // 关闭线程池 pool.shutdown(); }
16.8.2 Java 8 增强的ForkJoinPool
为多cpu和多核准备的线程池类
16.9 线程相关类(这个很重要)
16.9.1 ThreadLocal 类
ThreadLocal为每一个使用该变量的线程都提供了一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其他线程
的副本冲突.
T get()
void remove()
void set(T value)
class Account { /* 定义一个ThreadLocal类型的变量,该变量将是一个线程局部变量 每个线程都会保留该变量的一个副本 */ private ThreadLocal<String> name = new ThreadLocal<>(); // 定义一个初始化name成员变量的构造器 public Account(String str) { this.name.set(str); // 下面代码用于访问当前线程的name副本的值 System.out.println("---" + this.name.get()); } // name的setter和getter方法 public String getName() { return name.get(); } public void setName(String str) { this.name.set(str); } } class MyTest extends Thread { // 定义一个Account类型的成员变量 private Account account; public MyTest(Account account, String name) { super(name); this.account = account; } public void run() { // 循环10次 for (int i = 0 ; i < 10 ; i++) { // 当i == 6时输出将账户名替换成当前线程名 if (i == 6) { account.setName(getName()); } // 输出同一个账户的账户名和循环变量 System.out.println(account.getName() + " 账户的i值:" + i); } } } public class ThreadLocalTest { public static void main(String[] args) { // 启动两条线程,两条线程共享同一个Account Account at = new Account("初始名"); /* 虽然两条线程共享同一个账户,即只有一个账户名 但由于账户名是ThreadLocal类型的,所以每条线程 都完全拥有各自的账户名副本,所以从i == 6之后,将看到两条 线程访问同一个账户时看到不同的账户名。 */ new MyTest(at , "线程甲").start(); new MyTest(at , "线程乙").start (); } }
结果如下:
---初始名 null 账户的i值:0 null 账户的i值:1 null 账户的i值:0 null 账户的i值:2 null 账户的i值:1 null 账户的i值:3 null 账户的i值:2 null 账户的i值:3 null 账户的i值:4 null 账户的i值:4 null 账户的i值:5 null 账户的i值:5 线程乙 账户的i值:6 线程乙 账户的i值:7 线程甲 账户的i值:6 线程乙 账户的i值:8 线程乙 账户的i值:9 线程甲 账户的i值:7 线程甲 账户的i值:8 线程甲 账户的i值:9
16.9.2 包装线程不安全的集合
ArrayList,LinkedList,HashSet,TreeSet,HashMap,TreeMap等都是线程不安全的.
如果程序中有多个线程可能访问以上这些集合,就可以使用Collection提供的类方法把这些结婚包装成线程安全的集合.
HashMap<String,String> m = (HashMap<String, String>) Collections.synchronizedMap(new HashMap<String, String>());
16.9.3 线程安全的集合类
习题
1.写两个线程,一个线程打印1~52,另一个线程打印A~Z,打印顺序为12A34B56C...5152Z.该习题需要利用多线程通信的知识.
没看标准答案,下面这个是我自己写的,单个线程可以打印1~52或者A~Z,两个可以顺序打印,但是感觉实现方式很牵强,待改善.
class NumberTest implements Runnable{ Exercise exercise; public NumberTest(Exercise exercise){ this.exercise = exercise; } @Override public void run() { // TODO Auto-generated method stub for(int i = 1;i<27;i++){ try { exercise.printSomething(i); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } class Letter implements Runnable{ Exercise exercise; public Letter(Exercise exercise){ this.exercise = exercise; } @Override public void run() { // TODO Auto-generated method stub for(int i = 'A';i<='Z';i++){ try { exercise.printSomething(i); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } class Exercise{ int mark = 0; public synchronized void printSomething(int i) throws InterruptedException{ if(i>64){ System.out.println((char)i); notify(); wait(5); }else{ System.out.print(i*2-1); System.out.print(i*2); wait(5); notify(); } } } public class ThreadTest { public static void main(String[] args) throws InterruptedException{ Exercise exercise = new Exercise(); NumberTest numberTest = new NumberTest(exercise); Letter letter = new Letter(exercise); new Thread(numberTest).start(); Thread.sleep(2); new Thread(letter).start(); } }
把代码整理一下,如下
public class Test { public synchronized void printSomething(int i){ if (i > 64) { System.out.println((char) i); notify(); try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { System.out.print(i * 2 - 1); System.out.print(i * 2); try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } notify(); } } public static void main(String[] args){ Test test = new Test(); Runnable num = () -> { for (int i = 1; i < 27; i++) { test.printSomething(i); } }; Runnable letter = () -> { for (int i = 'A'; i <= 'Z'; i++) { test.printSomething(i); } }; new Thread(num).start(); new Thread(letter).start(); } }