进程
进程是一个应用程序(一个进程是一个软件)
两个进程之间的内存不共享
线程
线程是一个进程中的执行场景/执行单元。一个进程可以启动多个线程。
当输入“Helloworld”后,启动软件后。会先启动JVM,而JVM就是一个进程。JVM再启动一个主线程调用main方法。同时再启动一个垃圾回收线程负责看护,回收垃圾。java程序中至少有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程。
JVM中线程共享情况:
两个线程之间,堆内存和方法区内存共享。但是栈内存独立,一个线程一个栈。
多线程实现方法
启动线程start()
- start()方法的作用是:启动一个分支线程,在JWM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。
run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。
获取线程对象名字getName()
Thread t=new Thread();
String name=t.getName();
修改线程对象名字setName()
Thread t=new Thread();
t.setName("线程名字");
当线程没有设置名字的时候:
Thread-0
Thread-1
以此规律命名
当前线程对象currentThread()
Thread t=Thread.currentThread();
System.out.println(currentThread.getName());
//获取当前线程对象的名字
方法一.继承Thread
1.编写一个类,直接继承java.Lang.Thread,重写run方法。
public class Test1 {
public static void main(String[] args) {
ThreadTest t=new ThreadTest();
t.start();
for (int i = 0; i <10;i++) {
System.out.println("主线程"+i);
}
}
}
class ThreadTest extends Thread{
@Override
public void run() {
for (int i = 0; i <10;i++) {
System.out.println("分支线程"+i);
}
}
}
输出顺序随机
方法二.实现Runnable接口
2.编写一个类实现java .lang .Runnable接口。
- 创建一个可运行的对象,将可运行的对象封装成一个线程对象
调用创建的的类
public class Test1 {
public static void main(String[] args) {
Thread t=new Thread(new RunnableTest());
t.start();
for (int i = 0; i <10;i++) {
System.out.println("主线程"+i);
}
}
}
class RunnableTest implements Runnable{
@Override
public void run() {
for (int i = 0; i <10;i++) {
System.out.println("分支线程"+i);
}
}
}
调用匿名内部类
public class Test1 {
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i <10;i++) {
System.out.println("分支线程"+i);
}
}
});
t.start();
for (int i = 0; i <10;i++) {
System.out.println("主线程"+i);
}
}
方法三.实现Callable接口
- 优点:可以获取到线程的执行结果。
- 缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。
public static void main(String[] args) {
FutureTask f=new FutureTask(new CallableTest());
//创建一个未来任务类对象
Thread t=new Thread(f);
t.start();
Object o=f.get();
System.out.println(o);
}
class CallableTest implements Callable {
@Override
public Object call() throws Exception {
return "返回分支线程的结果";
}
}
结果:返回分支线程的结果
线程生命周期
周期中的状态:
- 1.新建状态
新建线程对象 - 2.就绪状态
抢夺CPU时间片的权利 - 3.阻塞状态
放弃占有的CPU时间片 - 4.运行状态
开始执行线程 - 5.死亡状态
运行结束
线程生命周期结构图:
线程睡眠
Thread. sleep()方法的使用
- 作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用。
sleep()是静态方法,里面传的参数是毫秒
public static void main(String[] args) {
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hello!");
}//让主线程延迟5秒后执行
注意:
无论用谁调用sleep()方法,sleep()方法都只会在当前写入的线程进入睡眠状态,与调用者无关
终止睡眠 interrupt()
终断当前线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)
public static void main(String[] args) {
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hello!");
}
class ThreadTest extends Thread {
@Override
public void run() {
try {
Thread.sleep(1000*100);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5; i++) {
System.out.println("分支线程" + i);
}
}
}//这段代码中主线程睡眠5秒,分支线程睡眠10秒,五秒后主线程开始执行程序
//并终止分支线程的睡眠
结果:
Hello!
分支线程0
分支线程1
分支线程2
分支线程3
分支线程4
注意:
run( )方法当中的异常不能throws ,只能try catch。因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常。
终止线程
强行终止线程
(这种方式存在很大的缺点:容易丢失数据。因为这种方式是直接将线程杀死了,线程没有保存的数据将会丢失。不建议使用。)
Thread t = new Thread( 线程对象);
t.stop();
合理终止线程
- 设置一个布尔类型的值,当想终止线程时,该布尔类型为false
public static void main(String[] args) {
ThreadTest tt=new ThreadTest();
Thread t=new Thread(new ThreadTest());
t.start();
tt.b=false;
}
class ThreadTest extends Thread {
boolean b=true;
@Override
public void run() {
if(true){
System.out.println("分支线程");
}
}
}
线程调度
常见的线程调度模型
抢占式调度模型:
- 那个线程的优先级比较高,抢到的CPU时间片的概率就高一些。java采用的就是抢占式调度模型。
均分式调度模型:
- 平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样。平均分配。
java中方法是和线程调度的关系
实例方法:
- setPriority (int newPriority)设置线程的优先级
- cetPriority获取线程优先级
最低优先级1
默认优先级是5
最高优先级10
静态方法:
- static void yield 让位方法
暂停当前正在执行的线程对象,并执行其他线程
yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。yield()方法的执行会让当前线程从运行状态回到就绪状态。
局部方法:
- void join ()合并线程
当前线程进入阻塞,分支线程执行,直到分支线程结束。当前线程才可执行
线程安全
存在线程安全问题的条件
- 1:多线程并发。
- 2:有共享数据。
- 3:共享数据有修改的行为。
解决线程安全问题
线程同步机制
线程同步就是线程排队了,线程排队就会牺牲一部分效率,但是线程安全了。
线程同步机制
线程同步机制的语法:
synchronized(){
线程同步代码块
}
synchronized后面小括号中传的这个"数据”是共享对象。这个对象必须是多线程共享的对象,也是多线程之间的标记。才能达到多线程排队。
在java语言中,任何一个对象都有一把锁,其实这把锁就是标记。(只是把它叫做锁。)
synchronized的三种写法:
1.同步代码块(常用)
模拟银行两个人对同一个用户取钱
public class User {
private String name;
private int balance;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
public User(String name, int balance) {
this.name = name;
this.balance = balance;
}
}
class withdraw extends Thread{
User u;
public withdraw(User u){
this.u=u;
}
@Override
public void run() {
synchronized (this){
int money=500;
u.setBalance(u.getBalance()-money);
System.out.println(Thread.currentThread().getName()+"--"+u.getBalance());
}
}
}
public static void main(String[] args) {
User u=new User("zhangsan",1000);
Thread t1=new withdraw(u);
Thread t2=new withdraw(u);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
执行原理:
- 1、假设t1和t2线程并发,开始执行以下代码的时候,会有一个先后顺序。
- 2、假设t1先执行,遇到了synchronized,这个时候自动找后面共享对象”的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代码块代码结束,这把锁才会释放。
- 3、假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后t2占有这把锁之后,进入同步代码块执行程序。这样就达到了线程排队执行。
锁池:
在这里找共享对象的对象锁线程进入锁池找共享对象的对象锁的时候,会释放之前占有的CPU时间片,有可能找到了,有可能没找到·没找到则在锁池中等待,如果找到了会进入就绪状态继续抢夺CPU时间片
2.在实例方法上使用synchronized
- 表示共享对象一定是this 并且同步代码块是整个方法体。
public synchronized void test( ){}
缺点:
1 synchronized出现在实例方法上,标记只能是this。不能是其他的对象。所以这种方式不灵活。
2.synchronized出现在实例方法上,表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低。所以这种方式不常用。
3.在静态方法上使用synchronized
- 表示在synchronized中的锁为类锁。且类锁只有1把。
解决开发中的线程安全问题
- 1.尽量使用局部变量代替实例变量和静态变量。
- 2.如果必须是实例变量,那么可以考虑创建多个对象,这样
实例变量的内存就不共享了。(一个线程对应1个对象,对象不共享,就没有数据安全问题了。) - 3.如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制。
死锁
是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。称此时系统处于死锁状态或系统产生了死锁。称这些永远在互相等待的进程为死锁进程。
产生死锁的必要条件:
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
模拟死锁
public static void main(String[] args) {
Object o1=new Object();
Object o2=new Object();
Thread t1=new ThreadTest1(o1,o2);
Thread t2=new ThreadTest2(o1,o2);
t1.start();
t2.start();
}
class ThreadTest1 extends Thread{
Object o1;
Object o2;
public ThreadTest1(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
public void run(){
synchronized (o1){//锁住o1,五秒后醒来,无法锁住o2,
//o2在下面类中被锁住
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
}
}
}
}
class ThreadTest2 extends Thread{
Object o1;
Object o2;
public ThreadTest2(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
public void run(){
synchronized (o2){//锁住o2,五秒后醒来,无法锁住o2,
try { //o1在上面类中被锁住
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
守护线程
java语言中线程分为两大类:
- 用户线程
- 守护线程(后台线程)
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束,用setDaemon()方法来设置一个线程为守护线程。其中垃圾回收线程(守护线程)。
注意:主线程main方法是一个用户线程。
public static void main(String[] args) {
hread t=new Thread(new DaemonThread());
t.setDaemon(true);
t.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
class DaemonThread extends Thread{
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println("守护线程"+"---"+i);
}
}
}
结果(多种结果中的一种):
守护线程---0
守护线程---1
守护线程---2
守护线程---3
守护线程---4
守护线程---5
main---0
main---1
main---2
守护线程---6
守护线程---7
main---3
main---4
即使守护线程还没有执行完,但是当用户线程执行完时,守护线程就会自动终止
定时器
- 按照特定的时间间隔执行一次任务
格式:
Timer t=new Timer();
t.schedule(定时任务,第一次执行时间,时间间隔);
创建一个定时任务:(每五秒执行一次)
public static void main(String[] args) {
Timer t=new Timer();
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date d=sdf.parse("2021-08-11 21:41:12");
t.schedule(new TimedTaskTest(),d,1000*5);
}
class TimedTaskTest extends TimerTask{
public void run(){
Date t=new Date();
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time=sdf.format(t);
System.out.println(time+"执行一次");
}
}
结果:
2021-08-11 21:43:46执行一次
2021-08-11 21:43:51执行一次
2021-08-11 21:43:56执行一次
...
生产者和消费者
wait和nonotify方法
- 1.wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是object类中自带的。wait方法和notify方法不是通过线程对象调用,
- 2.wait()方法作用: 表示让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止.
object o = new Object( ) ;
o. wait() ;
- 3.notify ()方法作用: 表示唤醒正在o对象上等待的线程。还有一个notifyAll()方法:这个方法是唤醒o对象上处于等待的所有线程。
object o = new Object( ) ;
o. notify() ;
生产者和消费者模式
模式定义:
生产线程负责生产,消费线程负责消费。生产线程和消费线程要达到均衡。
这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法(活动的当前线程进入等待状态,并且释放之前占有的o对象的锁)和notify方法(只会通知,不会释放之前占有的o对象的锁)
以下案例模拟生产者和消费者模式:
创建一个数组,生产者在数组中每生产一个对象,消费者在数组中就消费一个对象
public static void main(String[] args) {
List l=new ArrayList();
Thread t1=new Thread(new production(l));
Thread t2=new Thread(new consumption(l));
t1.setName("生产者");
t2.setName("消费者");
t1.start();
t2.start();
}
class production extends Thread{
List l;
public production(List l) {
this.l = l;
}
public void run(){
while (true){
synchronized (l){
if (l.size()==1){
try {
l.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Object o=new Object();
l.add(o);
System.out.println(Thread.currentThread().getName()+"--生产了--"+o);
l.notify();
}
}
}
}
class consumption extends Thread{
List l;
public consumption(List l) {
this.l = l;
}
public void run(){
while (true){
synchronized (l){
if (l.size()==0){
try {
l.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Object o=l.remove(0);
System.out.println(Thread.currentThread().getName()+"--消费了--"+o);
l.notify();
}
}
}
}
结果为:
生产者--生产了--java.lang.Object@7949fe25
消费者--消费了--java.lang.Object@7949fe25
生产者--生产了--java.lang.Object@6f1c6720
消费者--消费了--java.lang.Object@6f1c6720
...