1、什么是进程,什么是线程
1、进程的狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
2,线程:线程是程序中执行的线程,Java虚拟机允许程序同时运行多个执行线程。
3、进程和线程的关系:一个进程可以有多个线程
4、进程A和进程B内存独立不共享
5、线程A和线程B,方法区和堆内存共享,栈内存不共享,一个线程一个栈内
2、实现线程的两种方式
1、继承java.lang.Thread类,重写如何方法
public class test1 {
public static void main(String[] args) {
ThreadTest threadTest=new ThreadTest();
threadTest.start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程----》"+i);
}
}
}
class ThreadTest extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("子线程--》"+i);
}
}
}
2、实现在java.lang.Runable接口,实现run方法
package com.zzuli.thread;
public class test2 {
public static void main(String[] args) {
ThreadTest2 threadTest=new ThreadTest2();
Thread thread=new Thread(threadTest);
thread.start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程----》"+i);
}
}
}
class ThreadTest2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("子线程----》"+i);
}
}
}
注意:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其它的类,更灵活。
3、线程的生命周期
1,线程的生命周期有五种状态,分别是,新建状态,就绪状态,运行状态,阻塞状态,死亡状态。
2、图片展示
4、获取线程的基本信息
1、获取线程的名字:线程对象.getName()
2、设置线程的名字:线程对象.setName()
3、获取当前线程对象:static Thread currentThread() ;
3.1该方法是一个静态的方法,在哪个线程调用返回的就是哪个线程的对象
5、线程的睡眠和唤醒
1、sleep()方法
参数和方法类型:该方法是一个静态的方法,参数是毫秒值
作用:导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性。
2、interrupt()
2.1、作用:终止此线程的睡眠。这个方法是采用java异常处理机制来唤醒此线程的
例如:
//当把子线程沉睡一年,然后就可以用interrupt()方法唤醒
public class test3 {
public static void main(String[] args) {
ThreadTest3 threadTest=new ThreadTest3();
Thread thread=new Thread(threadTest);
thread.start();
}
}
class ThreadTest3 implements Runnable{
@Override
public void run() {
try {
Thread.sleep(1000*60*60*60*365);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 100; i++) {
System.out.println("子线程----》"+i);
}
}
}
这是可以看到控制台什么也没有打印出
2.2、这时可以调用interrupt方法唤醒此线程
public class test3 {
public static void main(String[] args) {
ThreadTest3 threadTest=new ThreadTest3();
Thread thread=new Thread(threadTest);
thread.start();
thread.interrupt();
}
}
class ThreadTest3 implements Runnable{
@Override
public void run() {
try {
Thread.sleep(10000*60*60*365);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("我被唤醒了---》");
}
for (int i = 0; i < 100; i++) {
System.out.println("子线程----》"+i);
}
}
}
运行结果为:
6、线程的调度
1、常见的线程调度模型有哪些
1.1 抢占式调度:那个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些。java采用的就是抢占式调度模型。
1.2 均分式调度:平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样。平均分配,一切平等。有一些编程语言,线程调度模型采用的是这种方式。
2、java中提供了哪些方法是和线程调度有关系的呢?
2.1实例方法:
void setPriority(int newPriority) 设置线程的优先级
int getPriority() 获取线程优先级
最低优先级1,默认优先级是5,最高优先级10,优先级比较高的获取CPU时间片可能会多一些。(但也不完全是,大概率是多的。)
2.2静态方法:
static void yield() 让位方法
注意:暂停当前正在执行的线程对象,并执行其他线程,yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用,yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。在回到就绪之后,有可能还会再次抢到。
2.3实例方法:
void join() //合并线程
class MyThread1 extends Thread {
public void doSome(){
MyThread2 t = new MyThread2();
t.join(); // 当前线程进入阻塞,t线程执行,直到t线程结束。当前线程才可以继续。
}
}
class MyThread2 extends Thread{
}
7、关于多线程并发环境下,数据的安全问题。
1、为什么要说这个: 以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。这些代码我们都不需要编写。 最重要的是:你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据在多线程并发的环境下是否是安全的。(重点:*****)
2、什么时候数据在多线程并发的环境下会存在安全问题呢?
三个条件:条件1:多线程并发。条件2:有共享数据。条件3:共享数据有修改的行为。满足以上3个条件之后,就会存在线程安全问题。
3、怎么解决线程安全问题呢?
使用“线程同步机制”。线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。
4、说到线程同步这块,涉及到这两个专业术语:
4.1异步编程模型:
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。其实就是:多线程并发(效率较高。)
异步就是并发。
4.2同步编程模型:
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型。效率较低。线程排队执行。
同步就是排队。
5、Java中有三大变量
实例变量:在堆中。
静态变量:在方法区。
局部变量:在栈中。
以上三大变量中:
5.1局部变量永远都不会存在线程安全问题。因为局部变量不共享。(一个线程一个栈。)局部变量在栈中。所以局部变量永远都不会共享。
5.2实例变量在堆中,堆只有1个。
5.3静态变量在方法区中,方法区只有1个。
5.4堆和方法区都是多线程共享的,所以可能存在线程安全问题。局部变量+常量:不会有线程安全问题。成员变量:可能会有线程安全问题。
5.5如果使用局部变量的话:建议使用:StringBuilder。因为局部变量不存在线程安全问题。选择StringBuilder。StringBuffer效率比较低。
ArrayList是非线程安全的。Vector是线程安全的。HashMap HashSet是非线程安全的。Hashtable是线程安全的。
6、synchronized有三种写法:
第一种:同步代码块
灵活
synchronized(线程共享对象){
同步代码块;
}
第二种:在实例方法上使用synchronized
表示共享对象一定是this
并且同步代码块是整个方法体。
第三种:在静态方法上使用synchronized
表示找类锁。
类锁永远只有1把。
就算创建了100个对象,那类锁也只有一把。
对象锁:1个对象1把锁,100个对象100把锁。
类锁:100个对象,也可能只是1把类锁。
7、死锁
1、java中导致死锁的原因:Java中死锁最简单的情况是,一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了。导致了死锁。这是最容易理解也是最简单的死锁的形式。但是实际环境中的死锁往往比这个复杂的多。可能会有多个线程形成了一个死锁的环路,比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1,这样导致了一个锁依赖的环路:T1依赖T2的锁L2,T2依赖T3的锁L3,而T3依赖T1的锁L1。从而导致了死锁。
从这两个例子,我们可以得出结论,产生死锁可能性的最根本原因是:线程在获得一个锁L1的情况下再去申请另外一个锁L2,也就是锁L1想要包含了锁L2,也就是说在获得了锁L1,并且没有释放锁L1的情况下,又去申请获得锁L2,这个是产生死锁的最根本原因。另一个原因是默认的锁申请操作是阻塞的。
2、死锁的代码演示
//synchronized中最好不要嵌套使用,很容易造成死锁
public class DeadThread6 {
public static void main(String[] args) {
Object o1=new Object();
Object o2=new Object();
//t1和t2线程共享o1,o2
DeadThread1 t1=new DeadThread1(o1,o2);
DeadThread2 t2=new DeadThread2(o1,o2);
t1.start();
t2.start();
}
}
class DeadThread1 extends Thread {
Object obj1;
Object obj2;
public DeadThread1(Object obj1, Object obj2) {
this.obj1 = obj1;
this.obj2 = obj2;
}
@Override
public void run() {
synchronized (obj1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2){
System.out.println("线程1");
}
}
}
}
class DeadThread2 extends Thread {
Object obj1;
Object obj2;
public DeadThread2(Object obj1, Object obj2) {
this.obj1 = obj1;
this.obj2 = obj2;
}
@Override
public void run() {
synchronized (obj2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1){
System.out.println("线程2");
}
}
}
}
8、线程的扩展
1,守护线程
java语言中线程分为两大类:
一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)。
设置线程变成守护线程:线程对象.setDaemon(true)
守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,
守护线程自动结束。
注意:主线程main方法是一个用户线程。
守护线程用在什么地方呢?
每天00:00的时候系统数据自动备份。
这个需要使用到定时器,并且我们可以将定时器设置为守护线程。
一直在那里看着,没到00:00的时候就备份一次。所有的用户线程
如果结束了,守护线程自动退出,没有必要进行数据备份了。
2,定时器
定时器的作用:间隔特定的时间,执行特定的程序。
每周要进行银行账户的总账操作。
每天要进行数据的备份操作。
在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见的,
那么在java中其实可以采用多种方式实现:
可以使用sleep方法,睡眠,设置睡眠时间,没到这个时间点醒来,执行
任务。这种方式是最原始的定时器。(比较low)
在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。
不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持
定时任务的。
在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,
这个框架只要进行简单的配置,就可以完成定时器的任务。
代码实现
public class TimerTest {
public static void main(String[] args) throws ParseException {
//创建定时器对象
Timer timer=new Timer();
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date= sdf.parse("2021-08-29 0:23:30:00");
timer.schedule(new LogTimerTask(), date, 1000 * 5);
}
}
//编写一个定时任务类
class LogTimerTask extends TimerTask{
@Override
public void run() {
//在这里你可以指定你要执行的任务
System.out.println("五秒执行一次");
}
}
3、实现线程的第三种方式:实现Callable接口。(JDK8新特性。)
这种方式实现的线程可以获取线程的返回值。之前讲解的那两种方式是无法获取线程返回值的,因为run方法返回void。
优点:可以获取到线程的执行结果
缺点:效率比较低,在获取t线程的执行结果的时候,当前线程手阻塞,效率较低
public class testThread5 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1,创建一个未来任务类对象
//参数非常重要,需要给一个callback接口实现类对象
FutureTask task=new FutureTask(new Callable() {
@Override
public Object call() throws Exception {//call方法相当于run方法
Thread.sleep(1000*10);
System.out.println("线程的第三种方法");
int a=100;
int b=200;
return a+b;
}
});
//创建线程
Thread t=new Thread(task);
//启动线程
t.start();
//这里是main方法,这是在主线程中,在主线程中,怎么获取t线程的返回结果
Object obj=task.get();//这里会导致当前线程阻塞
System.out.println("hello world");
}
}
4、关于Object类中的wait和notify方法。(生产者和消费者模式!)
第一:wait和notify方法不是线程对象的方法,是java中任何一个java对象
都有的方法,因为这两个方式是Object类中自带的。
wait方法和notify方法不是通过线程对象调用,
不是这样的:t.wait(),也不是这样的:t.notify()..不对。
第二:wait()方法作用?
Object o = new Object();
o.wait();
表示:
让正在o对象上活动的线程进入等待状态,无期限等待,
直到被唤醒为止。
o.wait();方法的调用,会让“当前线程(正在o对象上
活动的线程)”进入等待状态。
第三:notify()方法作用?
Object o = new Object();
o.notify();
表示:
唤醒正在o对象上等待的线程。
还有一个notifyAll()方法:
这个方法是唤醒o对象上处于等待的所有线程。