一、线程和进程
进程-操作系统中的组成部分
所有运行中的任务对应一个进程,当一个程序进入内存运行时,就变成了一个进程,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位
进程的特征
1.独立性:进程是系统独立存在的实体,他可以拥有自己独立的资源,每个进程都有自己私有的地址空间,在没有经过进程本身的允许下,一个用户的进程是不允许访问其他进程的地址空间
2.动态性:进程与程序的区别是,程序只是一个静态的指令集和,而进程是一个正在系统中活动的指令集和,在进程中加入了时间的感念,进程具有自己的生命周期和各种活动状态,这些在程序中是不存在的
3.并发性:多个进程可以再单个处理器上并发执行,多个进程之间不会互相影响
并发性和并行性是两个概念
并行性:指在同一时刻,有多条语句指令在多个处理器上同时执行
并发性:同一时刻只有一条指令执行,但多个进程指令被快速轮换执行,是的在宏观上看起来是多个指令共同执行的效果
线程-进程的执行单元
特征
线程是进程的组成部分,一个进程可以有多个线程,一个线程必须有一个父进程
线程可以拥有自己的堆栈,自己的程序计数器和自己的局部变量
线程将不在拥有系统资源,他与父进程的其他线程共享所拥有的全部资源
线程是独立运行的,他不知道进程中是否还有其他线程存在
同一个进程中的多个线程是并发运行的
总结一句话:一个程序运行后至少有一个进程,一个进程可以包含多个线程,但至少包含一个线程
多线程的优势
1.进程在执行的过程中拥有独立的内存单元,但是多个线程共享内存,从而极大地提高了程序的运行效率
2.线程比进程有更高的性能,创建进程需要为进程重新分配系统资源,但创建线程代价小。
3.Java语言内置了多线程功能支持,从而简化了Java多线程编程
二、线程的创建和启动
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例
继承Thread类创建线程类
1.定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体代表线程要完成的任务,
因此把run()方法称为线程执行体
2.创建Thread子类的实例,即创建线程对象。
3.调用线程对象的start()方法来启动多线程
package day10;
public class Test extends Thread {
public Test(String name){
super(name);
}
@Override
//重写run方法,run方法就是线程的执行体
public void run() {
for(int i=0;i<100;i++){
//直接调用Thread类的getName()方法返回当前线程的名字
System.out.println(getName()+""+i);
}
}
public static void main(String[] args) {
for(int i=0;i<100;i++){
//Thread类提供了currentThread()方法获取当前线程
System.out.println(Thread.currentThread().getName()+""+i);
if(i==20){
//创建线程
Test t1 = new Test("线程1");
Test t2 = new Test("线程2");
Test t3 = new Test("线程3");
Test t4 = new Test("线程4");
Test t5 = new Test("线程5");
//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
}
}
注意的是:在默认的情况下,主线程的名字是main
实现Runable接口创建线程
1.定义Runable接口的实现(implements)类,并重写改接口的run()方法,该run方法同样是线程的执行体
2.创建Runable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象
创建新线程的方法
.1.new Thread(new Runable())
2.Test1 test1 = new Test1();new Thread(test1,"新线程")
package day10;
public class Test1 implements Runnable {
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args) {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20){
Test1 test1 = new Test1();
new Thread(test1,"新线程1").start();
new Thread(test1,"新线程2").start();
}
}
}
}
//第二种
package day10;
public class Test3 {
public static void main(String[] args) {
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<50;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
});
t1.start();
}
}
实现Callable<T>接口
1.定义Callable接口的实现(implements)类,并重写改接口的run()方法
2.由于Thread和Ruhable都没有返回值,所有使用Callable(有返回值的),但Callable对象不能直接放到Thread中去(Thread并未提供),所以使用FutureTask<T>
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class Test {
//1.创建对象
Callable<String> callable = new MyCallableTest(100);
//2.生成FutureTask任务
FutureTask<String> f1 = new FutureTask<>(callable);
//3.交给线程池处理
Thread thread=new Thread(f1);
//4.启动线程
thread.start();
}
import java.util.concurrent.Callable;
public class MyCallableTest implements Callable<String>{
private int n;
MyCallableTest(int n){
this.n=n;
}
@Override
public String call() throws Exception {
int sum=0;
for (int i = 0; i <=n; i++) {
sum+=i;
}
return "结果是:"+n;
}
}
三、线程的生命周期
新建(New)就绪(Ready)运行(Runing)阻塞(Blocked)和死亡(Dead)
新建和就绪状态
当程序使用new关键字创建一个线程之后,该线程就处于新建状态,此时他和其他的Java对象一样,仅仅有Java虚拟机为其分配内存,只有当对象调用了start()方法之后,该线程才会处于就状态
package day10;
public class Test4 extends Thread {
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==5){
new Test4().run();
new Test4().run();
}
}
}
}
上面的程序是直接调用了run()方法,该种结果只有一个线程:主线程main,
只能对处于新建状态的线程调用start()方法,否则会引发IllegalThreadStateException
运行和阻塞
1.如果处于就绪状态的线程获得了CPU,开始执行run方法的线程执行体,则该线程处于运行状态
2.当一个线程开始运行时,他不可能一直处于运行状态,线程在运行过程中需要被中断,目的是是其他的线程获得执行的机会
3.只有当线程调用了他的sleep()或yield()方法后才会被放弃所占用的资源
进入阻塞的条件
1.线程调用了sleep()方法主动放弃所占用的资源
2.线程调用了一个阻塞式IO方法,再该方法返回之前,该线程被阻塞
3.线程视图获取一个同步监视器,但该同步监视器正被其他线程所持有的
4.线程在等待某个通知(notify)
5.程序调用了线程的suspend()方法将该线程挂起--容易导致死锁
解除上面的阻塞,重新进入就绪状态
1.调用了sleep()方法的线程经过了指定的时间
2.线程调用的IO方法已经返回
3.线程成功的获取了视图取得的同步监视器
4.线程正在等待某个通知时其他线程发出了一个通知
5.处于挂起的线程被调用了resume()恢复方法
线程死亡
1.run()或call()方法执行完成,线程正常结束
2.线程抛出一个未捕捉的Exception或Error
3.直接调用该线程的stop()方法来结束该线程(容易发动死锁,不建议使用)
isAlive()方法是判断线程的状态
死状态无法再次启动线程
package day10;
public class Test4 extends Thread {
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args) {
Test4 test4 = new Test4();
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==5){
test4.start();
System.out.println(test4.isAlive());
}
if(i>5&&!test4.isAlive()){
//视图启动线程
test4.start();
}
}
}
}
四、同步锁
1.使用synchronized关键字修饰方法,代码块,但不能修饰构造器,成员变量
package day10;
public class Account {
//封装账户的编号,账户余额的两个成员变量
private String accountNo;
private double balance;
public Account(){}
public Account(String accountNo,double balance){
this.accountNo=accountNo;
this.balance=balance;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//重写hashCode()和equals()方法
public int hashCode(){
return accountNo.hashCode();
}
public boolean equals(Object obj){
if(this==obj){
return true;
}
if(obj!=null && obj.getClass()==Account.class){
Account target=(Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
package day10;
public class DrawThread extends Thread {
//模拟用户账户
private Account account;
//所希望取得钱数
private double drawAmount;
public DrawThread(String name,Account account,double drawAmount){
super(name);
this.account=account;
this.drawAmount=drawAmount;
}
//当多个线程修改共享的数据时,将涉及数据安全的问题
@Override
public void run() {
//银行的余额大于索取的钱数
//加同步锁
synchronized (account) {
if (account.getBalance() >= drawAmount) {
System.out.println(getName() + "取钱成功,取出的余额是:" + drawAmount);
//修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("余额是:" + account.getBalance());
} else {
System.out.println(getName() + "余额不足,取钱失败");
}
}
}
}
//测试
package day10;
public class Test5 {
public static void main(String[] args) {
//创建一个用户
Account lpf = new Account("1234567", 1000);
//模拟两个线程取钱
new DrawThread("lpf",lpf,600).start();
new DrawThread("sjp",lpf,600).start();
}
}
2.使用lock,一般常用的是ReentrantLock
package day10;
import java.util.concurrent.locks.ReentrantLock;
public class SuoTest {
private final ReentrantLock lock=new ReentrantLock();
//封装两个成员变量
private String account;//编号
private double balance;//余额
//构造器
public SuoTest(){}
public SuoTest(String account,double balance){
this.account=account;
this.balance=balance;
}
public double getBalance() {
return balance;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
//为了防止编号重复,重写hashCode和equals
public int hashCode(){
return account.hashCode();
}
public boolean equals(Object obj){
if(this==obj){
return true;
}
if(obj!=null && obj.getClass()==SuoTest.class){
SuoTest st=(SuoTest)obj;
return st.getAccount().equals(account);
}
return false;
}
//定义一个余额修改方法
public void balanceUpdate(double updataBalance){
//加锁
lock.lock();
try{
if(balance>=updataBalance){
System.out.println(Thread.currentThread().getName()+"取出的钱数为:"+updataBalance);
try{
Thread.sleep(1);
}catch (InterruptedException it){
it.printStackTrace();
}
//修改余额
balance-=updataBalance;
System.out.println(Thread.currentThread().getName()+"剩余余额为:"+balance);
}
else {
System.out.println(Thread.currentThread().getName()+"余额不足,取出失败");
}
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
SuoTest s1=new SuoTest("10001", 1000);
s1.balanceUpdate(800);
s1.balanceUpdate(800);
}
}
五、线程相关常用方法
① wait()
wait(): 线程等待,会释放锁 (用于同步代码块或同步方法中,不然会报错),然后进入等待状态和notify()和notifyAll()一起使用。
wait(long timeout): 等待规定的时间,如果在规定时间内被唤醒就继续执行,如果超过规定时间也会继续向下执行。
public class TestWait implements Runnable {
private Object lock;
public TestWait(Object lock) {
this.lock = lock;
}
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
// 创建两个线程
Thread t1 = new Thread(new TestWait(lock),"t1");
Thread t2 = new Thread(new TestWait(lock),"t2");
t1.start();
t2.start();
}
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "准备进入等待状态");
try {
//唤醒等待中的线程,进入就绪状态
lock.notify();
//线程等待,释放锁
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "等待结束,继续执行");
}
}
}
运行结果:
t2准备进入等待状态
t1准备进入等待状态
t2等待结束,本线程继续执行
运行结果不唯一,但是总会有一个线程执行不完。
以上运行步骤如下:
1.t2线程运行,调用notify()方法唤醒等待中的线程(现在等待队列中没有线程),然后调用wait()方法进入等待队列。
2.t1线程运行,调用notify()方法唤醒等待中的线程(这时唤醒了t2线程,因为只有等待队列中只有t2,所以一定是t2获取cpu使用权),然后wait()方法进入等待队列。
3.t2线程继续向下运行,然后结束,t1还在等待队列,没有被唤醒,所以t1执行不完。
注意:被唤醒的线程,会从等待队列,进入就绪状态,然后去竞争cpu的使用权。
② sleep(long timeout)
sleep(): 线程睡眠,不会释放锁(规定时间到了之后继续执行线程) 。一搬用于模拟网络延时。
public class TestSleep implements Runnable {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
// 创建两个线程
Thread t1 = new Thread(new TestSleep(),"t1");
Thread t2 = new Thread(new TestSleep(),"t2");
t1.start();
t2.start();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "准备进入等待状态");
try {
//线程睡眠两秒之后,继续执行,不会释放锁。
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "等待结束,继续执行");
}
}
运行结果:
t1准备进入睡眠状态
t2准备进入睡眠状态
t1睡眠结束,继续执行
t2睡眠结束,继续执行
以上结果不唯一。
③ join()
join(): 指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。底层是利用wait()方法实现。可以应用于必须多个线程完成才能执行主线程,比如:三个人去酒店吃饭,三个都到酒店才能上菜。
正常执行
public class TestJoin {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() { //线程t1
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println("执行t1");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(new Runnable() { //线程t2
@Override
public void run() {
try {
Thread.sleep(3000);
System.out.println("执行t2");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
System.out.println("执行main");
}
}
运行结果:
执行main
执行t1
执行t2
让主线程(main)最后执行,让 t1 和 t2 先执行完成。
public class TestJoin {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() { //线程t1
@Override
public void run() {
try {
System.out.println("执行t1");
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(new Runnable() { //线程t2
@Override
public void run() {
try {
System.out.println("执行t2");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
t1.join(); //t1参与到当前线程(main)的执行中,所以主线程需要等待t1执行完成才会继续执行,但是t2不受影响、。
System.out.println("执行main");
}
}
运行结果:
执行t2
执行t1
执行main
或者
执行t1
执行t2
执行main
让 t1 和 t2 按顺序执行
public class TestJoin {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() { //线程t1
@Override
public void run() {
try {
System.out.println("执行t1");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(new Runnable() { //线程t2
@Override
public void run() {
try {
System.out.println("执行t2");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t1.join(); //t1参与到当前线程的执行中,所以主线程需要等待t1执行完成才会继续执行,这是t2需要等待t1执行完毕之后才能执行。
t2.start();
}
}
运行结果:
执行t1
执行t2
④ yield()
yield(): 线程让步(正在执行的线程),会使线程让出cpu使用权,进入就绪状态。
public class TestYield {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() { //线程t1
@Override
public void run() {
System.out.println("执行t1");
Thread.yield();//t1让出cpu使用权限,回到就绪状态
System.out.println("执行结束t1");
}
});
Thread t2 = new Thread(new Runnable() { //线程t2
@Override
public void run() {
System.out.println("执行t2");
System.out.println("执行结束t2");
}
});
t1.start();
t2.start();
}
}如果先执行t1,然后t1会让出线程,进入就绪状态,然后t2执行完成,t1再去竞争cpu的使用权,在继续执行。
⑤ notify()和notifyAll()
5.1、notify(): 随机唤醒一个在等待状态的线程,进入就绪状态。
public class TestNotify implements Runnable{
static Object lock = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(new TestNotify(), "t1");
Thread thread2 = new Thread(new TestNotify(), "t2");
thread1.start();
thread2.start();
try {
Thread.sleep(1000); //主线程睡眠,让线程t1或t2执行
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 获得了锁");
System.out.println("唤醒前" + thread1.getName() + "状态是" + thread1.getState());
System.out.println("唤醒前" + thread2.getName() + "状态是 " + thread2.getState());
lock.notify(); // 随机唤醒一个在等待状中的线程
}
try {
Thread.sleep(1000);//主线程睡眠,让线程t1或t2执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("唤醒后" + thread1.getName() + "状态是" + thread1.getState());
System.out.println("唤醒后" + thread2.getName() + "状态是" + thread2.getState());
}
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 开始执行");
try {
lock.wait(); // 进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 线程结束");
}
}
}
运行结果:
t1 开始执行
t2 开始执行
main 获得了锁
唤醒前t1状态是WAITING
唤醒前t2状态是 WAITING
t1 线程结束
唤醒后t1状态是TERMINATED
唤醒后t2状态是WAITING运行结果不唯一
由上面可以看出,notify()随机唤醒了一个线程,唤醒的是t1,也有可能唤醒的是t2。然后没被唤醒的线程一直处于等待状态,这样就会导致程序结束不了。
5.2、notifyAll(): 唤醒在等待队列的所有线程,进入就绪状态。
package com.navi.vpx;
public class TestNotify implements Runnable{
static Object lock = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(new TestNotify(), "t1");
Thread thread2 = new Thread(new TestNotify(), "t2");
thread1.start();
thread2.start();
try {
Thread.sleep(1000); //主线程睡眠,让线程t1或t2执行
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 获得了锁");
System.out.println("唤醒前" + thread1.getName() + "状态是" + thread1.getState());
System.out.println("唤醒前" + thread2.getName() + "状态是 " + thread2.getState());
lock.notifyAll(); // 唤醒全部在等待状态的线程
}
try {
Thread.sleep(1000);//主线程睡眠,让线程t1或t2执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("唤醒后" + thread1.getName() + "状态是" + thread1.getState());
System.out.println("唤醒后" + thread2.getName() + "状态是" + thread2.getState());
}
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 开始执行");
try {
lock.wait(); // 进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 线程结束");
}
}
}
运行结果:
t1 开始执行
t2 开始执行
main 获得了锁
唤醒前t1状态是WAITING
唤醒前t2状态是 WAITING
t2 线程结束
t1 线程结束
唤醒后t1状态是TERMINATED
唤醒后t2状态是TERMINATED
运行结果不唯一,有上面可得notifyAll()唤醒的是所有在等待状态的线程,使线程都进入就绪状态,然后竞争锁,最后全部执行完成,程序结束。
六、wait()和sleep()的区别?
① wait() 来自Object,sleep()来自Thread。
② wait()会释放锁,sleep()不会释放锁。
③ wait()只能用在同步方法或代码块中,sleep()可以用在任何地方。
④ wait()不需要捕获异常,sleep()需要捕获异常。
七、为什么 wait()、notify()、notifyAll()方法定义在 Object 类里面,而不是 Thread 类?
① 锁可以是任何对象,如果在Thread类中,那只能是Thread类的对象才能调用上面的方法了。
② java中进入临界区(同步代码块或同步方法),线程只需要拿到锁就行,而并不关心锁被那个线程持有。
③ 上面方法是java两个线程之间的通信机制,如果不能通过类似synchronized这样的Java关键字来实现这种机制,那么Object类中就是定义它们最好的地方,以此来使任何Java对象都可以拥有实现线程通信机制的能力。