线程的基础
任务的概念:是实现Runnable接口的实例,也称为可运行对象,任务必须在线程中执行
线程的概念:线程是一个任务从头至尾的执行流程的控制机制【注:现成不等同于任务】,在java中是Thread类创建的一个对象
两者的关系:线程本质上也是一个任务,Thread类自身也实现了Runnable接口,所以另外一种思路是:定义一个Thread的拓展类,然后实现run( )方法,再从客户端创建这个类的对象,并调用start( )方法来启动线程。但是这不是一个好方法,把任务和任务机制(线程,是为了控制流程)混合在一起了。将任务混在了控制机制里面。
Java语言对多线程的支持:有一些列的接口和类,提供了多线程的创建和运行,锁定资源和避免冲突
从计算机来看:一个cpu一次运行一个线程,多线程就可以让程序的反应更快,执行效率更高
Runnable接口:
只有一个run( )方法,JVM运行任务(可运行对象)时会自动调用这个方法。
任务和线程的应用:
1.创建一个任务类:
需要有一个定义好的任务类(实现Runnable和覆写override),然后用这个任务类的构造方法创建一个任务
2.创建一个线程:
直接调用Java提供的Thread类创建
Thread thread = new Thread(Task)
3.开始线程:
调用线程对象的start( )方法,告诉JVM准备开始运行,然后JVM会自动加载任务类的run方法
thread.start( )
简单的demo
package com.design.TaskAndThread;
public class TaskThreadDemo {
public static void main(String[] args) {
// 创建任务
Runnable printA = new PrintChar('a', 100);
Runnable printB = new PrintChar('b', 100);
Runnable print100 = new PrintNum(100);
// 创建线程
Thread thread1 = new Thread(printA);
Thread thread2 = new Thread(printB);
Thread thread3 = new Thread(print100);
//
thread1.start();
thread2.start();
thread3.start();
}
}
// 定义一个任务类
class PrintChar implements Runnable {
private char charToPrint;
private int times;
// 构建方法不只是为了构建一个对象时候才使用,也可以初始化属性(私有属性)
public PrintChar(char a, int t) {
this.charToPrint = a;
this.times = t;
}
// 覆写run方法,让JVM调用时知道怎么做
@Override
public void run() {
for (int i = 0; i < times; i++) {
System.out.println(charToPrint);
}
}
}
class PrintNum implements Runnable {
private int lastNum;
public PrintNum(int n) {
this.lastNum = n;
}
@Override
public void run() {
for (int i = 0; i <= lastNum; i++) {
System.out.println(i + " ");
}
}
}
demo里有三个程序,为了能够同时运行,创建了三个线程
注:如果在
thread1.start();
thread2.start();
thread3.start();
改变为直接调用run( )
thread1.run();
thread2.run();
thread3.run();
就只是在单独一个线程中执行该方法,没有新的线程启动,结果就是按顺序执行完thread1然后thread2然后thread3。
Thread类:
包含创建线程的构造方法以及控制线程的很多方法,线程的开始和暂定以及状态的判断和结束
在java中不建议用stop方法来停止线程,通过对Thread变量赋值为null来表明停止
常用的方法:
线程的两种暂停:
1.Thread.yield( ) 执行一次就临时暂停,让出cpu时间
在demo中的修改,在任务类中的run( )方法中添加
// 覆写run方法,让JVM调用时知道怎么做
@Override
public void run() {
for (int i = 0; i < times; i++) {
System.out.println(charToPrint);
// 添加暂停线程的方法,但不是运行一次就暂停一次
Thread.yield();
}
}
2.Thread.sleep(num) 将线程设置为休眠确保其他的线程启动,休眠时间为毫秒数, 会抛出一个必检异常IntettuptedException。如果一个循环中调用了sleep方法,就应该将循环放在try-catch中。这样一出现错误才会停止线程
体会一下这两者的区别
首先是正确的方式:
try {
while(....)
Thread.sleep(1000);
}
}
catch ( IntettuptedException ex) {
}
接下是错误的方式:
while(....){
try{
Thread.sleep(1000);
}
}
catch ( IntettuptedException ex) {
}
后面这种错误的方式会导致,即使线程被中断,它还继续执行,特别注意,IDE自动添加try-catch时,会造成第二种,一定要记得循环控制代码在try-catch里
demo中修改,一样在任务类中修改run( )方法
@Override
public void run() {
for (int i = 0; i <= lastNum; i++) {
System.out.println(i + " ");
try {
if (i >= 29) {
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
使一个线程等待另一个线程结束
anotherThread.join:在一个线程的run方法运行中加入另外一个thread.join(),然后原本运行的线程会等待这个后加入的anotherThread执行完(可以指定执行的时间)
@Override
public void run() {
// 这是直接声明一个线程并在里面装载了任务
Thread thread4 = new Thread(new PrintChar('Z', 100));
thread4.start();
for (int i = 0; i <= lastNum; i++) {
System.out.println(i + " ");
try {
// 这是通过休眠暂停一个线程,使用thread.join要注视掉,不然暂停的时候自动就运行不用join了
// if (i >= 29) {
// Thread.sleep(10);
// }
if (i == 50) {
thread4.join( );
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
线程的优先级
默认情况下,线程会继承生成它的线程的优先级(java中优先级范围为1~10),使用setPriority方法更改优先级,默认三个优先级为(1 小,5 正常, 10 大)JVM会选择优先级大的可运行线程,就是数字越大越早运行,如果线程优先级相同,则会用循环队列给它们分配相同的cpu份额,称为循环调度
注意:在编写程序的时候,最好使用常量来赋值,数字可能后期会更改,
因为低优先级的线程必须等待高优先级的线程运行完,所以为了避免高优先级的线程出现问题一直卡着导致低优先级没机会运行,所以高优先级会加入sleep或者yield方法来暂停。
线程池
线程池的作用:每个任务都会开始一个新的线程,这会限制吞吐并且造成性能降低。线程池来管理并发执行任务。简单点就是限制线程的数量
重要的接口和类:Executor接口来执行线程池中的任务,ExecutorService接口继承Executor接口,然后管理和控制任务,ExecutorService接口继承Executor接口,同样是把管理和创建的功能解耦的设计(复习前面的任务和线程的设计)。Executor类实现了Exexutor接口和ExecutorService的接口,实现了接口的execute方法,并且提供了创建Executor对象的静态方法
Executor类的两个重要方法:1.newFixedThreadPool(num)这是创建一个限制最大线程的线程池2.newCachedThreadPool()这是在当之前创建的线程可用时就不会创建新的,如果没有再创造新的线程,会为每一个任务都创建一个新的线程,所有的任务都会并发的进行
线程池的工作原理:如果线程池中的完成了一个任务,就能够重新执行另外一个任务。如果线程池中的所有线程r都不是处于空闲状态,在(executor.shutdown)关闭前出现一个错误关闭了一个线程,这时来了一个新的任务等待执行,就会创建一个新线程来代替。线程池中的线程在60s内都没有被使用就该终止它。
demo
// ====创建一个最大线程数的executor对象====
// Executor定义了一个限制最大线程的线程池,然后用执行器 管理接口声明
ExecutorService executor = Executors.newFixedThreadPool(3);
// ====提交任务到执行器====
// 线程执行器给线程装载任务
executor.execute(new PrintChar('E', 100));
executor.execute(new PrintChar('T', 50));
executor.execute(new PrintNum(100));
// ====关闭线程池(执行器)====
executor.shutdown();
线程同步
作用:协调相互依赖的线程的执行,如果同一个资源被多个线程同时访问,可能会出现破坏,数据与真实的不符合
step | b | task1 | task2 |
---|---|---|---|
1 | 0 | n = b + 1 | |
2 | 0 | n = b + 1 | |
3 | 1 | b = n | |
4 | 1 | b =n |
原因:是因为任务2覆盖了任务1的结果,因为两个同时访问了一个公共资源,称为竞争状态,如果一个类的对象在多线程程序中没有导致竞争状态,那才是线程安全的
demo这个demo里面运用了很好的封装,注意理解,然户在判断线程池任务时的while要好好琢磨一下
package com.design.TaskAndThread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AccountWithoutSynchronized {
private static Account account = new Account();
public static void main(String[] args) {
// ====在线程里运行,这里的线程池
ExecutorService executor = Executors.newCachedThreadPool();
// 这里通过循环创建了100个线程
for(int i = 0; i < 100; i ++) {
executor.execute(new AddPenyTask());
}
executor.shutdown();
// isTerminated 如果线程池中所有任务都终止,返回true
while (!executor.isTerminated()) {
}
System.out.println("账户是多少钱? " + account.getBalance());
}
// ====任务类,增加账户金钱,将具体的操作分剥开,纯粹调用方法
private static class AddPenyTask implements Runnable{
@Override
public void run() {
account.deposit(1);
}
}
// ====账户类,跟账户有关系的操作方法都在这里,提供方法让外面的调用====
private static class Account {
private int balance = 0;
private int getBalance() {
return balance;
}
// ====每次增加1====
public void deposit(int amount) {
int newBalance = amount + balance;
try {
// 增加效果,线程休眠一会
Thread.sleep(5);
} catch (InterruptedException e) {
}
balance = newBalance;
}
}
}
线程安全的方法:
达到线程安全就必须让线程没有竞争公共资源,就得把程序中的这部分限制起来,这部分叫做临界区。
方法一: 将临界区的方法加上限定关键字 synchronized,可以加在方法或是在语句上
方法二: 在执行之前加上一把锁,对于静态方法要在类上加锁,实例方法要给对象加锁。如果一个线程调用了一个对象的同步方法,首先要给对象(类)加锁,然后执行这个方法。最后解锁。在解锁之前,另外一个调用那个对象(类)的线程会被堵塞,知道解锁
synchronized关键字
给整个方法加上synchronized
public synchronized void deposit(int amount)
给语句加上synchronized同步语句
优点就是允许设置同步方法中的部分代码,而不必是整个方法,这大大增强了程序的并发能力
private static class AddPenyTask implements Runnable{
@Override
public void run() {
/* 这是不可行的方法,这里的this的参数必须是一个对象的引用
* synchronized (this) {
account.deposit(1);
}*/
synchronized (account) {
account.deposit(1);
}
}
}
这是在调用的时候
利用加锁同步
java中可以采用锁和状态来同步线程,
其实synchronized在执行前都是隐式的加上一把锁,也有显示的锁可以使用。
Lock接口:一个锁是Lock接口的实例,定义了加锁和释放锁的方法。锁也有newCondition()方法来创建任意个数的Condition对象,来进行线程间的通信。
ReentrantLock类:是Lock的一个实现,创建两种相互排斥的公平策略的锁。策略为真时,等待时间最长的线程首先得到锁,公平策略为假的时候,锁将随机给线程。一个公平锁的程序被多个线程访问时,整体性能可能比默认设置的差,但是在获取锁并且资源缺乏时可以更小的时间变化。
demo:
public static class Account {
// 创建一个锁
private static Lock lock = new ReentrantLock();
private int balance = 0;
public int getBalance() {
return balance;
}
public void deposit(int amount) {
// ====获取lock后加上try-catch最后finally释放锁====
// 获取锁
lock.lock();
try {
int newBalance = balance + amount;
Thread.sleep(5);
balance = newBalance;
} catch (InterruptedException e) {
} finally {
// 释放锁
lock.unlock();
}
}
}
在任务的执行方法中的公共资源前加锁,使用synchronized限定方法和语句的使用比显示锁简单,但是显示锁对同步剧透状态的线程更加直观和灵活。