JavaSE——多线程(一)(进程和线程概念,多线程安全问题,同步代码块,同步方法,Lock锁)

一.线程和进程的概念

1.进程
  • 概念:进程就是正在运行的程序,是系统进行资源分配和调用的独立单位,每一个进程都有他自己的内存空间和系统资源。
  • 多进程的意义:单进程计算机只能做一件事情,而我们现在的计算机都可以一遍玩游戏(游戏进程),一遍听音乐(音乐进程),我们常见的操作系统都是多进程操作系统,例如Windows,Linux等。
  • 思考:对于单核计算机来说,游戏进程和音乐进程是同时进行的吗?当然不是,因为CPU在某个时间点上只能做一件事情,计算机是在游戏进程和音乐进程间做着频繁切换,且切换速度很快,所以,我们感觉游戏和音乐在同时进行,其实并不是同时执行的。多进程的作用不是提高执行速度,而是提高CPU的使用率。
2.线程
  • 概念:每个进程内部又可以执行多个任务(比如我们在qq音乐里面可以一边听歌一边搜索歌曲),而这每个任务我们就可以看成是一个线程,是程序使用CPU的基本单位。所以进程是拥有资源的基本单位,线程是CPU调度的基本单位。
  • 多线程的意义:多线程不是提高应用程序的执行速度,而是为了提高应用程序的使用率。
    如何理解这句话呢?我们程序在运行的过程当中,都是在抢CPU的时间片(执行权),如果是多线程的程序,那么在抢到CPU的执行权的概率应该比单线程程序抢到的概率要大。那么也就是说,CPU在多线程程序中执行的时间要比单线程多,所以就提高了程序的使用率。但是即使是多线程程序,那么他们中的哪个线程能抢占到CPU的资源呢,这个是不确定的,所以多线程具有随机性。

二.并行和并发的区别

你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。 (不一定是同时的)
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。

1.并发

指应用能够交替执行不同的任务

2.并行

指应用能够同时执行不同的任务

四.Java程序的运行原理和JVM的启动

1.Java程序的运行

Java命令会启动java虚拟机,启动JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法,所以 main方法运行在主线程中。

2.JVM的启动是多线程的吗?

JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的。

三.多线程程序实现

由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。但是Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。但是呢?Java可以去调用C/C++写好的程序来实现多线程程序。由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,然后提供一些类供我们使用。我们就可以实现多线程程序了。

1.方式一:继承Thread类
(1)案例演示
package com.westmo3.demo3;
public class MyDemo1 {
    public static void main(String[] args) {
        System.out.println("主线程开始执行");
        MyThread thread = new MyThread();
        thread.start();//启动子线程
        System.out.println("主线程执行完毕");
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("子线程执行了");
    }
}
(2)Thread类中基本方法的使用
public final String getName():获取线程名称
public final void setName(String name):设置线程名称
public static Thread currentThread():获取当前执行的线程
public final int getPriority() :获取线程的优先级(线程的默认优先级是5)
public final void setPriority(int newPriority):设置线程的优先级
public static void sleep(long millis) :线程休眠
public final void join() 意思就是: 等待该线程执行完毕了以后,其他线程才能再次执行,注意事项: 在线程启动之后,在调用方法
public static void yield():	暂停当前正在执行的线程对象,并执行其他线程。
public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。 
public final void stop():停止线程的运行
public void interrupt():中断线程(这个翻译不太好),查看API可得当线程调用wait(),sleep(long time)方法的时候处于阻塞状态,可以通过这个方法清除阻塞

案例演示

package com.westmo3.demo3;
public class MyDemo1 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程开始执行");
        MyThread thread = new MyThread();
        thread.start();//启动子线程
        thread.sleep(2000);//让线程睡眠2s

        MyThread myThread = new MyThread("1号线程");//利用有参构造该线程起个名字
        MyThread myThread1 = new MyThread("2号线程");
        MyThread myThread2 = new MyThread("3号线程");
        myThread.start();
        myThread.join();//可以让多个线程顺序执行
        myThread1.start();
        myThread1.join();
        myThread2.start();
        myThread2.join();
        System.out.println("主线程执行完毕");
    }
}
class MyThread extends Thread{
    public MyThread() {
    }
    public MyThread(String s) {
        super(s);
    }
    @Override
    public void run() {
        //获取当前线程的线程名
        //System.out.println(this.getName()+"-子线程执行了");
        System.out.println(Thread.currentThread().getName()+"-子线程执行了");
    }
}
2.方式二:实现Runnable接口
package com.westmo3.demo3;
public class MyDemo2 {
    public static void main(String[] args) {
        MyThread1 myThread1 = new MyThread1();
        Thread thread = new Thread(myThread1);
        thread.start();
        Thread thread1 = new Thread(myThread1);
        thread1.start();
    }
}
class MyThread1 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+"="+i);
        }
    }
}
3.方式三:实现 Callable 接口。 相较于实现 Runnable 接口的方式,方法可以有返回值,并且可以抛出异常。
package com.westmo3.demo3;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class MyDemo3 {
    public static void main(String[] args) {
        thread thread = new thread();
        FutureTask<Integer> task = new FutureTask<Integer>(thread);
        Thread thread1 = new Thread(task);
        thread1.start();
    }
}
class thread implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+"="+i);
        }
        return 200;
    }
}

五.案例分析引出线程安全问题

案例:某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。

分析:根据题目我们知道,这三个窗口买票是同时进行的,我们可以用三个线程来表示这三个窗口。
package com.westmo3.demo3;
public class MyDemo4 {
    public static void main(String[] args) {
        mythread th1 = new mythread();
        mythread th2 = new mythread();
        mythread th3 = new mythread();
        th1.setName("窗口1");
        th2.setName("窗口2");
        th3.setName("窗口3");
        th1.start();
        th2.start();
        th3.start();
    }
}
class mythread extends Thread{
    static int piao=100;//设置为共享变量,因为三个线程共同卖这100张
    @Override
    public void run() {
        while(true){
            if (piao>0) {
                try {
                    Thread.sleep(10);//用线程睡眠来演示网络延迟
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(this.getName()+"正在出售"+(piao--)+"票");
            }
        }
    }
}

从代码运行可以看出,出现了同票,0票甚至负票的情况,这些问题其实就是多线程环境下的数据安全问题。当其中一个线程抢得执行权执行时,当这个线程还没有执行完时,另一个线程又抢到了执行权。这就是问题发生得本质。

解决办法:把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。
(1)同步代码块方式
  • 格式
synchronized(对象){ //同步代码代码块上的锁,是一个互斥锁。
		死循环
		需要同步的代码;
	}
  • 弊端:它虽然解决了多线程得安全问题,但当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
  • 案例演示
package com.westmo3.demo3;
public class MyDemo {
    public static void main(String[] args) {
        mythread1 th1 = new mythread1();
        mythread1 th2 = new mythread1();
        mythread1 th3 = new mythread1();
        th1.setName("窗口1");
        th2.setName("窗口2");
        th3.setName("窗口3");
        th1.start();
        th2.start();
        th3.start();
    }
}
class mythread1 extends Thread{
    static int piao=100;//设置为共享变量,因为三个线程共同卖这100张
    static Object obj=new Object();//三个线程的锁对象必须是同一个
    @Override
    public void run() {
        while(true) {
            synchronized (obj) {//线程进入同步代码块,就持有了锁,那么 其他两个线程,就在同步代码块外面等着
                if (piao > 0) {
                    try {
                        Thread.sleep(20);//用线程睡眠来演示网络延迟
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(this.getName() + "正在出售" + (piao--) + "票");
                }
            }
           // 这个线程出了同步代码块,就 会把锁对象释放掉,这时候三个线程再次争抢时间片
        }
    }
}
(2)同步方法:就是把关键字加到方法上

同步方法的锁对象: 是this
静态同步方法的锁对象:就是当前类对应的字节码文件对象

package com.westmo3.demo3;
public class MydDemo5 {
    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        Thread th1 = new Thread(mythread);
        Thread th2 = new Thread(mythread);
        Thread th3 = new Thread(mythread);
        th1.setName("窗口1");
        th2.setName("窗口2");
        th3.setName("窗口3");
        th1.start();
        th2.start();
        th3.start();
    }
}
class Mythread implements Runnable{
static int piao=100;
int i=1;
    @Override
    public void run() {
        while(true) {
            if(i%2==0) {
                Buypiao();//同步方法
            }else {
                Buypiao1();//静态同步方法
            }
        }
    }
    public synchronized void Buypiao1() {//同步方法的锁对象是this
            if (piao>0) {
                try {
                    Thread.sleep(20);//用线程睡眠来演示网络延迟
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"正在出售"+(piao--)+"票");
            }
    }
    public static synchronized void Buypiao() {//静态同步方法的锁对象是当前类的字节码对象
        if (piao>0) {
            try {
                Thread.sleep(20);//用线程睡眠来演示网络延迟
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"正在出售"+(piao--)+"票");
        }
    }
}
(3)Java中锁的知识
  • java的内置锁:每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。 线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。 获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法
    java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,
    当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,
    直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去
  • java的对象锁和类锁:java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。

六.JDK1.5之后的Lock锁的使用

1.概述

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。

2.使用:void lock() 加锁 ,void unlock() 释放锁
3.案例演示
package com.westmo3.demo3;

import java.util.concurrent.locks.ReentrantLock;

public class MyDemo5 {
    public static void main(String[] args) {
        MyThrad1 myThrad1 = new MyThrad1();
        Thread th1 = new Thread(myThrad1);
        Thread th2 = new Thread(myThrad1);
        Thread th3 = new Thread(myThrad1);
        th1.setName("窗口1");
        th2.setName("窗口2");
        th3.setName("窗口3");
        th1.start();
        th2.start();
        th3.start();
    }
}
class MyThrad1 implements Runnable{
static int piao=100;
static ReentrantLock reentrantLock=new ReentrantLock();
    @Override
    public void run() {
        while(true){
            reentrantLock.lock();
            if(piao>0){
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"正在出售"+(piao--)+"票");
            }
            reentrantLock.unlock();
        }
    }
}
发布了58 篇原创文章 · 获赞 13 · 访问量 1873

猜你喜欢

转载自blog.csdn.net/weixin_44324174/article/details/104451513