目录
一、定义:
- 进程是一个应用程序(软件)。
- 线程是一个进程中的执行单元,一个进程有多个线程。
- 例如:进程是公司,线程是公司中的员工。main方法线程,gc垃圾回收器线程。
- 不同进程的内存独立,不共享。
- 不同线程,堆内存和方法区内存共享,但栈内存不共享,一个线程独享一个栈,栈之间互不干扰,叫多线程并发。
- 多线程是为了提高程序的处理效率。
二、实现线程的方法:
第一种:继承Thread类,重写run()方法,调用start()方法开启线程。
(1)start()方法的作用:在JVM中开辟一个新的栈空间,启动线程。启动的线程会在栈的底部自动调用run()方法。如果不用start而是用run,则不会开辟内存空间。
(2)代码:
public class Test{
public static void main(String args[]){//main线程,主线程
TestThread test=new TestThread();//创建线程对象
test.start();//start方法开启线程
for(int i=0;i<10;i++)
System.out.println("主线程:"+i);
}
}
class TestThread extends Thread {
public void run(){//run方法线程体
for(int i=0;i<10;i++){
System.out.println("分支线程:"+i);
}
}
}
//可以看到主线程和run线程同时执行,而不是顺序执行
(3)执行结果:
(4)内存图:
第二种:实现runnable接口,重写run方法,执行线程需要丢入runnable接口实现类,调用start方法。
(1)start()方法的作用:在JVM中开辟一个新的栈空间,启动线程。启动的线程会在栈的底部自动调用run()方法。如果不用start而是用run,则不会开辟内存空间。
(2)代码:
public class Test{
public static void main(String args[]){//main线程,主线程
TestThread test=new TestThread();//创建runnable接口实现类对象
Thread thread = new Thread(test);//创建线程对象(代理)
thread.start();//start方法开启线程
for(int i=0;i<100;i++)
System.out.println("主线程:"+i);
}
}
//可以看到主线程和run线程同时执行,而不是顺序执行
//runnable接口优点:所有线程可以共享一个类
class TestThread implements Runnable {
public void run(){//run方法线程体
for(int i=0;i<10;i++){
System.out.println("分支线程:"+i);
}
}
}
//可以看到主线程和run线程同时执行,而不是顺序执行
(3)运行结果:
三、线程生命周期:引入时间片概念
四、常用方法:
- 获取线程对象的名字:
线程对象.getName();
- 修改线程对象的名字:
线程对象.setName(name);
- 获取“当前”线程对象:
Thread t = Thread.currentThread();
是一个静态方法,不需要用线程对象.currentThread(),直接用Thread.currentThread()。
4.“当前”线程进入休眠:
Thread.sleep(time);
是一个静态方法,所以说如果在main方法中调用支线程.sleep(time),休眠的是main线程而不是支线程。
5.终止线程的休眠:
线程对象.Interrupt();
终止休眠靠的是try…catch的异常处理机制,Interrupt()方法执行后try语句块中还在进行sleep()的代码会报错,并终止sleep()执行,进入catch语句块。
6.终止线程的执行:
线程类中加一个成员变量boolean flag=true,run()方法中加一个if(flag==false){return;}条件判断,如果需要终止线程,调用setFlag()方法将flag置false即可。
五、线程调度:
- 常见的线程调度模型:
(1)抢占式调度模型:
线程优先级高的线程抢到CPU时间片的概率就高一些。java采用的就是抢占式模型。
(2)均分式调度模型:
平均分配CPU时间片,每个线程占有CPU时间片的时间一样。
2.Java中与线程调度有关的方法:
(1)void serPriority(int newPriority)设置线程优先级
(2)int getPriority()获取线程优先级
最低优先级1
默认优先级5
最高优先级10
(3)static void yield()让位方法,暂停当前正在执行的线程对象,从运行状态回到就绪状态。注意:回到就绪状态后还可能会再次抢到时间片。
(4)void join();t.join()当前线程进入阻塞,t线程先执行,t执行完后当前线程再继续执行。
六、线程安全:重点
-
什么时候多线程并发会存在安全问题?
多线程并发且对共享数据有修改的行为。 -
如何解决多线程安全问题?
线程同步机制(线程排队执行)。
线程同步会牺牲一部分效率。 -
两个模型:
(1)同步线程模型:
线程t1执行的时候,必须等待线程t2执行结束。属于多线程同步模型,效率较低,安全。
(2)异步线程模型:
线程各自执行,互不影响,属于多线程并发模型,效率高,不安全。 -
线程同步机制的语法:
synchronized(){
}
- ()中写需要排队执行的多个线程的共享对象。
假设t1.t2.t3.t4.t5五个线程。只希望t1.t2.t3排队,那么()写的是t1.t2.t3的共享对象,而这个对象对t4.t5来说不共享。 - {}中写不同线程需要排队执行的代码。
synchronized 实例方法名(){}
- 如果共享对象是this,并且需要同步的代码是整个方法体,建议用这种方式。否则用同步代码块的方式。
-
线程同步的实现原理:
(1)java中每个对象都有一把锁。
(2)假设t1.t2线程并发,开始执行代码的时候,肯定有先有后。
(3)假设t1抢到时间片,先执行了,遇到synchronized,这个时候自动找()中共享对象的锁,找到之后占有这把锁,然后执行同步代码块中的程序,直到同步代码块执行完,t1才会释放这把锁。
(4)假设t1已经占有这把锁,此时t2抢到时间片,也遇到synchronized,也会去占有()中共享对象的锁,但是这把锁已经被t1占有了,t2只能在同步代码块外面等待t1结束,直到t1释放锁,此时t2才可能抢到锁,进入自己的同步代码块执行。
(5)占有到共享对象执行代码块。占有不到共享对象不执行代码块。 -
例子:模拟银行取款
(1)场景:甲乙分别在前台和自助取款机对张三账户取款,丙在自助取款机对李四账户取款。
(2)代码:
public class Test{
public static void main(String args[]) {
Account account1 = new Account("张三",10000);//创建账户对象张三
AccountThread thread1 = new AccountThread(account1);//创建线程对象,对account1操作
thread1.setName("自助取款机-001");
AccountThread thread2 = new AccountThread(account1);//创建线程对象,account1操作
thread2.setName("前台");
thread1.start();//开启线程,模拟甲在自助取款机-001对张三的账户取款
thread2.start();//开启线程,模拟乙在前台对张三的账户取款
Account account2 = new Account("李四",7000);//创建账户对象李四
AccountThread thread3 = new AccountThread(account2);//创建线程对象,account2操作
thread3.setName("自助取款机-002");
thread3.start();//开启线程,模拟丙在自助取款机-002对张三的账户取款
}
}
class Account {
private String ID;
private int balance;
public Account() {
}
public Account(String ID, int balance) {
this.ID = ID;
this.balance = balance;
}
public String getID() {
return ID;
}
public void setID(String ID) {
this.ID = ID;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
public void drawMoney(int money){
// synchronized (this) {//this表示当前对象
int before = this.getBalance();
int after = before-money;
this.setBalance(after);
// }
}
}
class AccountThread extends Thread{//线程类
Account account;
public AccountThread(Account account){
this.account = account;
}
public void run(){
account.drawMoney(5000);//取款5000
System.out.println("用户在"+Thread.currentThread().getName()+"对账户"+account.getID()+"取款成功,余额:"+account.getBalance());
}
}
(3)若不用synchronized关键字,可能出现的情况:
甲乙同时取款,甲取完款后余额没有及时更新,乙抢到时间片进入取款代码,即更新余额的代码没有抢到时间片,此时甲乙都是以10000元的余额开始取款,甲乙同时取5000后可能余额还是5000。
(4)使用synchronized关键字后,就不会出现上述不安全情况。但是注意:线程1和2会线程同步,排队执行,但是线程3不会排队,因为线程1,2是操作的同一个对象张三,线程3是操作的另一个对象李四。
即:若张三账户在前台取款则该账户同一时间不能在取款机取款,只有在前台取完款,余额发生变化后,张三账户才能在取款机取款。而这个机制对李四账户没有影响,李四可以在任何时间取款。
(7)面试题:
public class Test{
public static void main(String args[]) {
Test1 test = new Test1();
Test1Thread tt1 = new Test1Thread(test);
Test1Thread tt2 = new Test1Thread(test);
tt1.setName("t1");
tt2.setName("t2");
tt1.start();
try {
Thread.sleep(1000);//目的是为了先开启线程tt1
} catch (InterruptedException e) {
e.printStackTrace();
}
tt2.start();
}
}
class Test1 {
public synchronized void dosome(){
System.out.println("dosome begin");
try {
Thread.sleep(1000*4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("dosome end");
}
public void doother(){
System.out.println("doother begin");
System.out.println("doother end");
}
}
class Test1Thread extends Thread{
Test1 test1;
public Test1Thread(Test1 test1){
this.test1 = test1;
}
public void run(){
if (Thread.currentThread().getName().equals("t1")){
test1.dosome();
}
if (Thread.currentThread().getName().equals("t2")){
test1.doother();
}
}
}
问题1:doother()方法执行的时候是否需要等待dosome()方法的结束?
答:不需要。因为doother()方法没有synchronized。tt1线程先执行,占有test对象的锁,执行dosome()方法,tt2线程若抢到时间片,则tt2线程执行,tt2执行doother()方法不需要获取对象test的锁,所以不需要等待。
问题2:doother()方法加上synchronized后执行的时候是否需要等待dosome()方法的结束?
答:需要。因为对象才有锁,方法没有锁,锁的是test对象而不是方法。tt1线程先执行,占有test对象的锁,执行dosome()方法,tt2线程若抢到时间片,tt2会去占有test锁,但是锁已经被tt1占了,所以tt2会等待test1锁被释放再执行。
public class Test{
public static void main(String args[]) {
Test1 test1 = new Test1();
Test1 test2 = new Test1();
Test1Thread tt1 = new Test1Thread(test1);
Test1Thread tt2 = new Test1Thread(test2);
tt1.setName("t1");
tt2.setName("t2");
tt1.start();
try {
Thread.sleep(1000);//目的是为了先开启线程tt1
} catch (InterruptedException e) {
e.printStackTrace();
}
tt2.start();
}
}
class Test1 {
public synchronized void dosome(){
System.out.println("dosome begin");
try {
Thread.sleep(1000*4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("dosome end");
}
public synchronized void doother(){
System.out.println("doother begin");
System.out.println("doother end");
}
}
class Test1Thread extends Thread{
Test1 test1;
public Test1Thread(Test1 test1){
this.test1 = test1;
}
public void run(){
if (Thread.currentThread().getName().equals("t1")){
test1.dosome();
}
if (Thread.currentThread().getName().equals("t2")){
test1.doother();
}
}
}
问题3:doother()方法执行的时候是否需要等待dosome()方法的结束?
不需要。因为两个线程是针对不同对象的。
问题4:synchronized 都前加static 变成静态方法后,doother()方法执行的时候是否需要等待dosome()方法的结束?
需要。synchronized出现在静态方法上是类锁,锁的是类,一个类只有一把锁。只有对象和类才有锁,别的没有锁。
(8)死锁:
两个线程互锁,程序一直不能往下执行,应该避免。开发的时候尽量不要synchronized嵌套synchronized。
七、守护线程:
- java中线程分为两大类:
一是:用户线程。
二是:守护线程(后台线程),如垃圾回收器线程。 - 守护线程特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程就自动结束。
3.实现代码:
public class Test{
public static void main(String args[]) {
Thread t= new TestThread();
t.setDaemon(true);//将线程变为守护线程
}
}
class TestThread extends Thread{
public void run(){
while(true){
System.out.println(Thread.currentThread().getName());
}
}
}
八、定时器:
timer.schedule(定时任务对象,第一次执行时间,间隔多久执行一次);
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class Test{
public static void main(String args[]) throws ParseException {
Timer timer = new Timer();
//Timer timer = new Timer(true);//守护线程的方式
SimpleDateFormat sdf = new SimpleDateFormat("yy-mm-dd hh:mm:ss");
Date firsttime = sdf.parse("2022-8-6 16:15:15");
timer.schedule(new LogTimerTask(),firsttime,1000*2);
}
}
class LogTimerTask extends TimerTask{
@Override
public void run() {
//编写定时器需要执行的任务
SimpleDateFormat sdf = new SimpleDateFormat("yy-mm-dd hh:mm:ss");
String s = sdf.format(new Date());
System.out.println(s+"完成一次两秒内的数据备份");
}
}
九、实现线程的第三种方式(带返回值):
实现Callable接口,实现call()方法。
优点:可以获取线程的返回值。
缺点:效率低。
使用get()方法获取线程返回值。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test{
public static void main(String args[]) throws ExecutionException, InterruptedException {
FutureTask fu = new FutureTask(new ThreadTest());//FutureTest类代理
Thread t = new Thread(fu);
t.start();
Object s = fu.get();
System.out.println(s);
}
}
class ThreadTest implements Callable{
@Override
public Object call() throws Exception {
int a=10;
int b=10;
return a+b;
}
}
十、Object类的wait和notify方法:
- java中任何一个对象都有这两个方法。
- wait()方法和notify()方法不是通过线程对象调用。
- 两种方法必须建立在线程同步的基础上。
- 方法作用:
(1)wait()方法的作用:让正在o对象上活动的线程进入等待状态,并且释放o的锁,直到被唤醒。
(2)notify()方法的作用:唤醒o对象上正在等待的线程。notifyAll()唤醒o所有线程。 - 生产者和消费者模式:
(1)什么是生产者消费者模式:
对同一个仓库对象o,生产线程负责生产,消费线程负责消费,生产线程和消费线程达到均衡。
(2)案例:
仓库采用list集合。list集合中智能存储1个元素。1个元素就表示仓库满了。如果list集合中元素个数是0,就表示仓库空了。
import java.util.ArrayList;
import java.util.List;
public class Test{
public static void main(String[] args) {
List list = new ArrayList();//仓库
Thread producer = new Thread(new Producer(list));//生产者线程
Thread consumer = new Thread(new Consumer(list));//消费者线程
producer.setName("生产者线程");
consumer.setName("消费者线程");
producer.start();
consumer.start();
}
}
class Producer implements Runnable{
List list;
public Producer(List list) {
this.list = list;
}
@Override
public void run() {
while(true) {//死循环一直生产
synchronized (list) {
if (list.size() >=1) {//仓库满
try {
list.wait();//生产者线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else{//仓库未满
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName()+"生产了"+obj.toString());
list.notifyAll();//唤醒等待的线程,继续消费
}
}
}
}
}
class Consumer implements Runnable{
List list;
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
while(true) {//死循环一直消费
synchronized (list) {
if (list.size() <= 0) {//仓库空
try {
list.wait();//消费者线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else{
Object obj = list.remove(0);
System.out.println(Thread.currentThread()+"购买了"+obj.toString());
list.notifyAll();//唤醒等待的线程,继续生产
}
}
}
}
}