1、什么是进程
百度百科:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行
2、什么是线程
百度百科:线程(thread)是操作系统能够进行运算
3、进程与线程之间的关系
-
线程属于进程的实体,也就是说明线程是必须依赖于进程
-
进程里面至少有一个线程, 没有线程此进程也无法运行
-
一个进程允许有多个线程
4、进程的特点
-
独立性:进程是系统进行资源分配和调度的一个独立单位
-
动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的
-
并发性(支持多进程):任何进程都可以同其他进程一起并发执行
-
结构性:进程由程序(控制进程执行的指令集),数据(数据集合是程序在执行时所需要的数据和工作区)和进程控制块(程序控制块包含进程的描述信息和控制信息是进程存在的唯一标志)三部分组成
5、线程的特点
-
允许跨线程资源共享
-
创建线程的开销要比进程小(进程要分配一大部分的内存,而线程只需要分配一部分栈就可以了)
-
提高进程运行的效率
6、创建线程的三种方式
a、继承 Thread 类
class MyThread extends Thread{ //继承Thread类,作为线程的实现类
private String name ; // 表示线程的名称
public MyThread(String name){
this.name = name ; // 通过构造方法配置name属性
}
public void run(){ // 覆写run()方法,作为线程的操作主体
for(int i=0;i<10;i++){
System.out.println(name + "运行,i = " + i) ;
}
}
public class ThreadDemo02{
public static void main(String args[]){
// 实例化对象
MyThread mt1 = new MyThread("线程A ") ;
MyThread mt2 = new MyThread("线程B ") ;
//启动线程
mt1.start() ;
mt2.start() ;
}
}
b、实现 Runnable 接口
class MyThread implements Runnable{ // 实现Runnable接口,作为线程的实现类
private String name ; // 表示线程的名称
public MyThread(String name){
this.name = name ; // 通过构造方法配置name属性
}
public void run(){ // 覆写run()方法,作为线程的操作主体
for(int i=0;i<10;i++){
System.out.println(name + "运行,i = " + i) ;
}
}
public class RunnableDemo01{
public static void main(String args[]){
// 实例化对象
MyThread mt1 = new MyThread("线程A ") ;
MyThread mt2 = new MyThread("线程B ") ;
// 实例化Thread类对象
Thread t1 = new Thread(mt1) ;
Thread t2 = new Thread(mt2) ;
// 启动线程
t1.start() ;
t2.start() ;
}
}
实现Runnable接口创建线程也可以通过匿名内部类来完成
public static void main(String[] args) {
new Thread(new Runnable() {
实现Runnable接口创建线程还可以通过Lambda表达式来完成
public static void main(String[] args) {
new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}).start();
}
注意事项
-
Thread类本身也实现了Runnable接口(看源码)
-
不能反复调用同一个线程的start()方法,会抛异常
-
线程启动虽然调用的是 start() 方法,但实际上调用的却是 run() 方法定义的主体
-
不调用start()方法,直接调用run()方法并没有创建线程,不具备多线程效果(https://www.cnblogs.com/heqiyoujing/p/11355264.html参考资料
c、实现Callable接口
直接继承Thread和实现Runnable接口都有一个缺陷就是:在执行完任务之后无法获取执行结果。但是实现Callable接口方法可以获取执行结果
首先,我们需要知道以下类或接口:
-
Future接口
-
对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。
-
-
RunnableFuture接口
-
RunnableFuture继承了Runnable接口和Future接口
-
-
FutureTask
-
FutureTask实现了RunnableFuture接口,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
-
代码实现
public class MyThread implements Callable<Long> {
也可以写成匿名内部类
public static void main(String[] args) throws ExecutionException, InterruptedException {
new Thread(new FutureTask<Long>(new Callable<Long>() {
还可以写成Lambda表达式
public static void main(String[] args) throws ExecutionException, InterruptedException {
new Thread(new FutureTask<Long>(()->{
//输出线程名和线程id
System.out.println(Thread.currentThread().getName()+" "+Thread.currentThread().getId());
//返回线程id
return Thread.currentThread().getId();
})).start();
}
7、继承 Thread 类和实现Runnable接口两种方式的区别
-
实现Runnable 接口,可以方便实现资源的共享
-
继承 Thread类,则不适合于多个线程共享资源
8、volatile关键字
如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的(简单提一下,具体内容不过多阐述,还需要自己查阅资料)
9、线程的生命周期
-
线程从创建到销毁的过程,包含五个状态:
-
新建(new一个线程)
-
就绪:有执行资格,没有执行权
-
运行:有执行资格,有执行权
-
阻塞:由于一些操作让线程处于该状态,没有执行资格,没有执行权,但另一些操作可以使它激活,激活后处于就绪状态
-
死亡:线程对象变成垃圾,等待回收
-
-
线程状态转换图解
在此提出一个问题,Java 程序每次运行至少启动几个线程?
回答:至少启动两个线程,每当使用 Java 命令执行一个类时,实际上都会启动一个 JVM,每一个JVM实际上就是在操作系统中启动一个线程,Java 本身具备了垃圾的收集机制。所以在 Java 运行时至少会启动两个线程,一个是 main 线程,另外一个是垃圾收集线程。
10、控制线程的方法
a、sleep()和wait()的区别
-
sleep是线程中的方法,但是wait是Object中的方法。
-
sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中(重点)
-
sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
-
sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。
注意一点:阻塞被唤醒后还是从原来被阻塞的点开始往下执行
b、join()
join在线程里面意味着“插队”,哪个线程调用join代表哪个线程插队先执行——但是插谁的队是有讲究了,不是说你可以插到队头去做第一个吃螃蟹的人,而是插到在当前运行线程的前面,比如系统目前运行线程A,在线程A里面调用了线程B.join方法,则接下来线程B会抢先在线程A面前执行,等到线程B全部执行完后才继续执行线程A。
-
代码体验
/** 定义的线程类 * */
class MyThread extends Thread {
当系统中正在运行多个线程时,join()到底是暂停了哪些线程?t.join()方法会使所有线程都暂停并等待t的执行完毕吗?
答:t.join()方法只会使主线程(或者说调用t.join()的线程)进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程。
c、yield()
由运行态到就绪态,停止一下后再由就绪态到运行态
-
暂停当前正在执行的线程对象,并执行其他线程。
-
意思就是调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,
-
但是yield不能立刻交出CPU,会出现同一个线程一直执行的情况,另外,yield 方法
-
注意调用
代码体验
//创建一个线程类
class MyThread extends Thread {
d、中断线程
参考资料https://www.cnblogs.com/liyutian/p/10196044.html
e、设置线程优先级
java 中的线程优先级的范围是1~10,默认的优先级是5。10最高。
可以通过getPriority()获取线程优先级,setPriority()设置线程优先级。设置线程的优先级应该在线程启动之前。
f、后台线程
参考资料https://blog.csdn.net/staticFinal_shuaibi/article/details/84865688
10、解决线程同步问题
当一个类中的属性被多个线程共享,那么就会造成一种问题,如果这多个线程要操作同一个资源时就有可能出现资源同步问题。
实现线程同步有很多种方法,这里讲三种(文末资料有补充)
a、同步方法
-
用synchronized关键字修饰方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
-
代码体现
public class Bank {
private int count = 0;// 账户余额
// 存钱
public synchronized void addMoney(int money) {
count += money;
System.out.println(System.currentTimeMillis() + "存进:" + money);
}
// 取钱
public synchronized void subMoney(int money) {
if (count - money < 0) {
System.out.println("余额不足");
return;
}
count -= money;
System.out.println(+System.currentTimeMillis() + "取出:" + money);
}
// 查询
public void lookMoney() {
System.out.println("账户余额:" + count);
}
}
b、同步代码块
-
用synchronized关键字修饰语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
-
同步代码块格式
synchronized(同步对象){
需要同步的代码
} -
代码体现
public class Bank {
private int count = 0;// 账户余额
// 存钱
public void addMoney(int money) {
synchronized(this){
count += money;
}
System.out.println(System.currentTimeMillis() + "存进:" + money);
}
// 取钱
public void subMoney(int money) {
synchronized(this){
if (count - money < 0) {
System.out.println("余额不足");
return;
}
count -= money;
}
System.out.println(+System.currentTimeMillis() + "取出:" + money);
}
// 查询
public void lookMoney() {
System.out.println("账户余额:" + count);
}
}
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
c、显示加锁
-
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法具有相同的基本行为和语义,并且扩展了其能力。
-
ReenreantLock类的常用方法有:
-
ReentrantLock() : 创建一个ReentrantLock实例
-
lock() : 获得锁
-
unlock() : 释放锁
-
-
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
-
-
代码体验
public class Bank {
private int count = 0;// 账户余额
// 需要声明这个锁
private Lock lock = new ReentrantLock();
// 存钱
public void addMoney(int money) {
lock.lock();
try {
count += money;
System.out.println(System.currentTimeMillis() + "存进:" + money);
} finally {
lock.unlock();
}
}
// 取钱
public void subMoney(int money) {
lock.lock();
try {
if (count - money < 0) {
System.out.println("余额不足");
return;
}
count -= money;
System.out.println(+System.currentTimeMillis() + "取出:" + money);
} finally {
lock.unlock();
}
}
// 查询
public void lookMoney() {
System.out.println("账户余额:" + count);
}
}1、ReentrantLock()还可以通过public ReentrantLock(boolean fair)构造方法创建公平锁,即,优先运行等待时间最长的线程,这样大幅度降低程序运行效率。 2、关于Lock对象和synchronized关键字的选择: (1)、最好两个都不用,使用一种java.util.concurrent包提供的机制,能够帮助用户处理所有与锁相关的代码。 (2)、如果synchronized关键字能够满足用户的需求,就用synchronized,他能简化代码。 (3)、如果需要使用更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally中释放锁。
11、线程同步之死锁
同步可以保证资源共享操作的正确性,但是过多同步也会产生问题。例如,现在张三想要李四的画,李四想要张三的书,张三对李四说“把你的画给我,我就给你书”,李四也对张三说“把你的书给我,我就给你画”两个人互相等对方先行动,就这么干等没有结果,这实际上就是死锁的概念。
所谓死锁,就是两个线程都在等待对方先完成,造成程序的停滞,一般程序的死锁都是在程序运行时出现的。
-
代码体验
class Zhangsan{ // 定义张三类
public void say(){
System.out.println("张三对李四说:“你给我画,我就把书给你。”") ;
}
public void get(){
System.out.println("张三得到画了。") ;
}
}
class Lisi{ // 定义李四类
public void say(){
System.out.println("李四对张三说:“你给我书,我就把画给你”") ;
}
public void get(){
System.out.println("李四得到书了。") ;
}
}
public class ThreadDeadLock implements Runnable{
// 实例化static型对象(注意这里必须是static修饰s,因为静态成员依赖类存在只加载一次,不是静态就不止加载一次,对象不同,锁不同)
private static Zhangsan zs = new Zhangsan() ;
// 实例化static型对象(注意这里必须是static修饰,因为静态成员依赖类存在只加载一次,不是静态就不止加载一次,对象不同,锁不同)
private static Lisi ls = new Lisi() ;
private boolean flag = false ; // 声明标志位,判断那个先说话
public void run(){ // 覆写run()方法
if(flag){
synchronized(zs){// 同步张三
//张三说话
zs.say() ;
try{
//线程睡眠
Thread.sleep(500) ;
}catch(InterruptedException e){
e.printStackTrace() ;
}
synchronized(ls){//同步李四
//张三得到画
zs.get() ;
}
}
}else{
synchronized(ls){//同步李四
//李四说话
ls.say() ;
try{
//线程睡眠
Thread.sleep(500) ;
}catch(InterruptedException e){
e.printStackTrace() ;
}
synchronized(zs){
//李四得到书
ls.get() ;
}
}
}
}
public static void main(String args[]){
// 控制张三
ThreadDeadLock t1 = new ThreadDeadLock() ;
// 控制李四
ThreadDeadLock t2 = new ThreadDeadLock() ;
t1.flag = true ;
t2.flag = false ;
//创建线程
Thread thA = new Thread(t1) ;
Thread thB = new Thread(t2) ;
//启动线程
thA.start() ;
thB.start() ;
}
}
-
程序进入死锁状态
12、线程通信
当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法准确
线程通信通常用于生产者/消费者模式
a、传统的线程通信
假设现在系统中有两个线程,这两个线程分别代表存款者和取钱者----现在有一种特殊的要求:存款者和取钱者不断地重复存款、取款动作,而且要求每当存款者将钱存入指定账户后,取钱者就立即取出该笔钱。不允许连续存款和取款。
为了实现这种功能,可以借助于Object类提供的wati()、notify()、notifyAll()三个方法。下面是关于这三个方法的解释:
-
wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()或notufyAll()来唤醒该线程(带参数则等待指定时间后自动苏醒)
-
notify():唤醒在
-
notifyAll():唤醒在
代码体验
/**
*账户类
*/
public class Account {
private double balance;//存款
private int id;//编号
private boolean flag=true;//判断账户是否有存款
public Account() {
}
public Account(double balance, int id) {
this.balance = balance;
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public double getBalance() {
return balance;
}
/**
* 取钱
*/
public synchronized void draw(int money){
if (flag){//账户有存款
System.out.println(DrawThread.currentThread().getName()+"取钱"+money);
//取钱
balance-=money;
System.out.println("账户余额为"+balance);
//账户无存款
flag=false;
//唤醒存钱线程
notifyAll();
}else{//账户没存款
//线程等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 存钱
*/
public synchronized void deposit(int money){
if (!flag){//账户没存款
System.out.println(DrawThread.currentThread().getName()+"存钱"+money);
//存钱
balance+=money;
System.out.println("账户余额为"+balance);
//账户有存款
flag=true;
//唤醒取款
notifyAll();
} else{//账户有存款
//线程等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 存钱线程
*/
public class DepositThread extends Thread{
private Account account;