进程:
正在运行的程序,是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和资源。
线程:
是进程的单个顺序控制流,或者说就是一个单独执行的路径
一个进程如果只有一条执行路径,称之为单线程
一个进程如果有多条执行路径,称之为多线程
线程是包含在进程中。
举例:扫雷,360杀毒软件,百度网盘
了解三个关键词:
1、串行,指的是一个程序中所有的任务都是按照先后顺序执行的,在前一个任务还没有处理完的情况下,是不会进行处理下一个任务的
举例:理发店只有一个理发师,很多人去理发,先等前面的人理完发,再轮到后面的人。
2、并行,指的是将任务分给不同的处理器去处理,每一个处理器中再进行串行处理。
举例:火车站上有很多个窗口,多个窗口同时卖票,但是针对于一个窗口来说,一个人的一个人的去卖票
3、并发,实质上是一种现象,并发需要处理器的支持,比如在出库一个任务的时候操作系统可以进行调用再处理其他的任务,不论串行还是并行
都需要操作系统的支持并发。假设喝水是一个任务,每个火车站售票员,再售票的同时也能喝水,这就表示支持并发。
JVM启动的时候是单线程还是多线程呢?
多线程:
main(主线程)
垃圾回收线程
所以在JVM启动的时候,最低要求要有两个线程存在,所以JVM启动的时候是多线程的。
创建线程的第一种方式:
继承Thread类
1、创建一个自定义类继承Thread类
2、这个类要重写Thread类中的run方法 为什么是run()方法呢? 当线程启动之后,执行的代码逻辑仅是run()方法的代码逻辑
3、根据这个类创建线程对象
4、启动线程
public class MyThreadDemo2 {
public static void main(String[] args) {
//每创建一个对象,相当于创建一个新的线程对象
// MyThread1 myThread1 = new MyThread1();
//启动线程
// myThread1.run();
// myThread1.run();
//单纯的调用run方法仅仅表示的是一个对象调用普通的方法,所以这里依旧是单线程程序
//要想看到多线程的效果,就必须换一种方式启动线程 start()
//面试题:调用start()与调用run()的区别
//run()方法中仅仅是封装了被线程执行的代码,但是呢,直接调用run()与调用普通的方法方式没有任何区别
//start()方法的调用,首先单独启动了一个线程,然后再由JVM去调用该线程类中的run()方法
// myThread1.start();
//当一个线程对象启动多次的时候,报错:IllegalThreadStateException 非法的线程状态异常
// myThread1.start();
System.out.println("==============================");
//模拟多线程环境
//至少创建2个及两个以上的线程对象
MyThread1 myThread1 = new MyThread1();
MyThread1 myThread2 = new MyThread1();
//启动线程
myThread1.start();
myThread2.start();
/**
* 注意事项:
* 1、启动线程调用的是start()方法
* 2、线程的先后启动顺序,对结果没有影响
*/
}
}
public class MyThread1 extends Thread{
@Override
public void run() {
//写我们要线程执行的逻辑代码
// System.out.println("数加科技,yyds");
//一般来说,被线程执行的逻辑代码都是比较耗时的,为了模拟这里的耗时,我这里使用循环打印
for(int i=1;i<200;i++){
System.out.println(i);
}
}
}
如何给线程设置名字呢?
通过构造方法给线程起名字:
Thread(String name) 分配一个新的 Thread对象。
如何获取线程的名字呢?
public final String getName()返回此线程的名称
public class MyThreadDemo3 {
public static void main(String[] args) {
//创建线程对象
//通过构造方法给线程起名字
//由于我们要模拟多线程环境,所以创建线程的个数为2个或2个以上
// MyThread2 t1 = new MyThread2("一号");
// MyThread2 t2 = new MyThread2("二号");
// t1.start();
// t2.start();
MyThread2 t1 = new MyThread2();
MyThread2 t2 = new MyThread2();
MyThread2 t3 = new MyThread2();
//public final void setName(String name)将此线程的名称更改为等于参数name 。
t1.setName("一号");
t2.setName("二号");
t3.setName("二号");
//启动线程
t1.start();
t2.start();
t3.start();
//如何获取main方法所在的线程呢?
//public static Thread currentThread()返回对当前正在执行的线程对象的引用。
// System.out.println(Thread.currentThread().getName());
}
}
public class MyThread2 extends Thread {
public MyThread2() {
}
public MyThread2(String name) {
super(name);
}
@Override
public void run() {
System.out.println("========================");
for (int i = 1; i <= 200; i++) {
System.out.println(getName() + ":" + i);
}
}
}
我们在前几个代码中都没有设置优先级,我们猜测一定会有一个默认的优先级。
默认的优先级是多少呢?5
获取线程优先级的方法:
public final int getPriority()返回此线程的优先级。
设置线程优先级的方法:
public final void setPriority(int newPriority)更改此线程的优先级
public final static int MAX_PRIORITY = 10; 线程可以拥有的最大优先级。
public final static int MIN_PRIORITY = 1; 线程可以拥有的最小优先级。
总结:
1、线程的默认优先级是5
2、线程优先级的范围是1-10
3、线程优先级高仅仅表示的获取CPU时间片的机率会高一些,但是呢,并不是绝对会获取到
public class ThreadPriorityDemo {
public static void main(String[] args) {
MyPriorityThread t1 = new MyPriorityThread();
MyPriorityThread t2 = new MyPriorityThread();
MyPriorityThread t3 = new MyPriorityThread();
//获取t1,t2,t3线程的优先级
// System.out.println(t1.getPriority());
// System.out.println(t2.getPriority());
// System.out.println(t3.getPriority());
//设置优先级
//IllegalArgumentException 非法的参数传入
// t1.setPriority(100);
//newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY
t1.setPriority(10);
t2.setPriority(1);
t3.setPriority(2);
//给线程起名字
t1.setName("一号");
t2.setName("二号");
t3.setName("三号");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
public class MyPriorityThread extends Thread{
@Override
public void run() {
for (int i = 1; i <= 200; i++) {
System.out.println(getName() + ":" + i);
}
}
import java.util.Properties;
/*
user: root
password: 123456
*/
public class PropertiesDemo {
public static void main(String[] args) {
Properties properties = new Properties();
properties.setProperty("user", "root");
properties.setProperty("password", "123456");
String user = properties.getProperty("user");
String password = properties.getProperty("password");
System.out.println(user + "--" + password);
}
}
演示休眠线程的效果
public class ThreadSleepDemo {
public static void main(String[] args) {
MySleepThread t1 = new MySleepThread();
MySleepThread t2 = new MySleepThread();
MySleepThread t3 = new MySleepThread();
//给线程设置名字
t1.setName("1号");
t2.setName("2号");
t3.setName("3号");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
public class MySleepThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 200; i++) {
System.out.println(getName() + ":" + i);
//加入休眠的方法
//public static void sleep(long millis)
//停1秒钟 1秒=100毫秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
加入线程
public final void join()
线程对象调用join的目的是让当前线程先执行完毕,完毕后其他的线程再进行执行
换句话说,就是其他的线程等待调用join的线程执行完毕后再执行。
注意:
join的调用必须在start之后。
public class ThreadJoinDemo {
public static void main(String[] args) {
MyJoinThread t1 = new MyJoinThread();
MyJoinThread t2 = new MyJoinThread();
MyJoinThread t3 = new MyJoinThread();
//给线程设置名字
t1.setName("1号");
t2.setName("2号");
t3.setName("3号");
//启动线程
t1.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
t3.start();
}
}
public class MyJoinThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 200; i++) {
System.out.println(getName() + ":" + i);
}
}
}
礼让线程:
public static void yield() 暂停当前正在执行的线程对象,并执行其他线程 它的作用是为了让多个线程之间运行的时候看起来更加和谐,但是呢,并不能保证多个线程一人一次。
public class ThreadYieldDemo {
public static void main(String[] args) {
//创建2个线程对象模拟多线程
MyYieldThread t1 = new MyYieldThread();
MyYieldThread t2 = new MyYieldThread();
//给线程起名字
t1.setName("1号");
t2.setName("2号");
//启动线程
t1.start();
t2.start();
}
}
public class MyYieldThread extends Thread{
@Override
public void run() {
for(int i=1;i<=200;i++){
System.out.println(getName()+":"+i);
Thread.yield();
}
}
}
后台线程:
(守护线程)
public final void setDaemon(boolean on)
Java中有两类线程:用户线程,守护线程
用户线程:在学习多线程之前,运行起来的一个个的线程都是用户线程
守护线程:所谓守护线程,指的是程序在运行的时候,在后台提供了一个通用的服务线程。比如说垃圾回收线程,他就是一个守护线程
并且这种线程并不是一定存在的,所以反过来说,只要程序存在守护线程,程序就不会终止。
守护线程怎么去设置呢?
public final void setDaemon(boolean on)
注意:
1、守护线程必须在启动之前进行设置
2、当运行的程序只有一个线程的时候并且这个线程是守护线程的时候,Java虚拟机退出(程序停止)
public class ThreadDaemonDemo {
public static void main(String[] args) {
//创建3个线程
MyDaemonThread t1 = new MyDaemonThread();
MyDaemonThread t2 = new MyDaemonThread();
MyDaemonThread t3 = new MyDaemonThread();
//给线程起名字
t1.setName("刘备");
t2.setName("关羽");
t2.setDaemon(true);
t3.setName("张飞");
t3.setDaemon(true);
//启动线程
t1.start();
t2.start();
t3.start();
}
}
public class MyDaemonThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 200; i++) {
System.out.println(getName() + ":" + i);
}
}
}
多线程的实现方案二:实现Runnable接口
1、自定义一个类实现Runnable接口
2、实现run()方法
3、创建自定义类对象
4、创建Thread线程对象,将自定义的对象作为参数传递到构造方法中
public class MyRunnableDemo1 {
public static void main(String[] args) {
//创建自定义类对象
MyRunnable1 myRunnable1 = new MyRunnable1();
//创建2个线程对象
Thread t1 = new Thread(myRunnable1);
Thread t2 = new Thread(myRunnable1);
//给线程起名字
t1.setName("明旺");
t2.setName("王宇");
t1.start();
t2.start();
}
}
public class MyRunnable1 implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 200; i++) {
//由于Runnable接口中没有getName()方法,所以这里无法使用获取线程对象名字
//间接调用,我们可以先获取当前线程的对象,然后再调用Thread类中getName()方法
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
两种方式实现
继承Thread类
public class SellTicketDemo1 {
public static void main(String[] args) {
//创建3个售票窗口,模拟3个窗口同时卖票
TicketWindow1 window1 = new TicketWindow1();
TicketWindow1 window2 = new TicketWindow1();
TicketWindow1 window3 = new TicketWindow1();
//给线程起名字
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
//启动线程
window1.start();
window2.start();
window3.start();
}
}
public class TicketWindow1 extends Thread {
private static int tickets = 100;
@Override
public void run() {
//在run方法中定义100张票是有问题的
//每个线程都会执行run方法,这样的话,相当于每个线程都有自己的那100张票
//所以,我们将100张票定义在这里是不合适的。
// int tickets = 100;
while (true) {
if (tickets > 0) {
System.out.println(getName() + "正在出售第" + (tickets--) + "张票");
}
}
}
}
实现Runnable接口
public class SellTicketDemo2 {
public static void main(String[] args) {
TicketWindow2 ticketWindow2 = new TicketWindow2();
//2、创建3个线程对象,模拟3个窗口
Thread window1 = new Thread(ticketWindow2);
Thread window2 = new Thread(ticketWindow2);
Thread window3 = new Thread(ticketWindow2);
//3、给线程对象起名字
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
//4、启动线程
window1.start();
window2.start();
window3.start();
}
}
public class TicketWindow2 implements Runnable{
private int tickets=100;
@Override
public void run() {
while (true) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
}
}
}
}
为了更加接近现实,我们加入延迟sleep
但是呢,加入延迟后,产生了两个问题:
1、相同的票我们卖了多次 CPU的操作是原子性导致的,CPU中小小时间片足矣运行多次
2、出现了第0张票和负数的票 线程的执行具有随机性和延迟性导致的,加入sleep后,线程变成阻塞状态,让其他线程执行。
public class SellTicketDemo3 {
public static void main(String[] args) {
TicketWindow3 ticketWindow2 = new TicketWindow3();
//2、创建3个线程对象,模拟3个窗口
Thread window1 = new Thread(ticketWindow2);
Thread window2 = new Thread(ticketWindow2);
Thread window3 = new Thread(ticketWindow2);
//3、给线程对象起名字
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
//4、启动线程
window1.start();
window2.start();
window3.start();
}
}
public class TicketWindow3 implements Runnable{
private int tickets = 100;
@Override
public void run() {
//窗口1,窗口2,窗口3
while (true) {
//窗口1,窗口2,窗口3
if (tickets > 0) { // 1
//窗口1,窗口2,窗口3
//为了模拟更加真实的售票场景,我们加入延迟
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
//窗口1正在出售第100张票
//窗口2正在出售第100张票
//出现重复的原因是,CPU的操作是原子性,
//由于tickets--是两步操作,先赋值输出再--
//当窗口1赋值输出后,还没有来得及--,这时候窗口2也执行到了这一步,此时tickets的值还没有发生变化
//所以出现了相同的票卖了多次。
//理想状态下:
//窗口1正在出售第100张票
//窗口2正在出售第99张票
//出现第0张票的现象解释:
//两个或3个窗口同时在tickets的值为1的时候,都进入到if之中,都会进行一次睡眠
//当第一个窗口睡眠结束,也打印结束,此时的tickets的值从1变成0
//所以当后面的线程睡眠结束打印结果是0
//负数的来源是当tickets的值为1的时候,三个线程都进入if语句可能会造成的现象。
}
}
}
}
上一个案例加入了延迟操作,出现的问题,其实就称之为:线程安全问题。
要想去解决这个问题,就要搞清楚哪些原因导致的问题出现:
(三点总结出是否会出现线程安全问题,缺一不可)
1、是否存在多线程环境
2、是否存在共享数据/共享变量
3、是否有多条语句操作着共享数据/共享变量
回想一下上一个案例是否满足判读线程安全问题的条件:
1、是否存在多线程环境 存在,有3个窗口线程
2、是否存在共享数据/共享变量 存在,共享数据是100张票
3、是否有多条语句操作着共享数据/共享变量 是
三个条件都满足,由此可见,我们上一个案例出现问题是一个正常的现象,因为它同时满足以上3个条件
如何解决这些问题呢?
第1,2条件是我们无法改变的,我们只能想办法改变第3个条件,只要其中一个不满足,就不会发生线程安全问题。
解决问题的思想:
要是有一个办法可以将多条语句操作共享数据的代码给包成一个整体,在某个线程执行的时候,别的线程进不来就可以了。
直到某个线程执行完一次run方法后,其他线程才能进入执行。
Java提供了一个机制给我们使用,来解决线程安全的问题:同步安全机制
解决方案一:同步代码块
语句格式:
格式:
synchronized(对象){需要同步的代码;}
1、这里的对象是什么呢?
随便创建一个对象试试
2、需要同步的代码又是哪些呢?
多条语句操作共享数据的代码
public class SellTicketDemo4 {
public static void main(String[] args) {
//1、创建自定义类对象
TicketWindow4 ticketWindow2 = new TicketWindow4();
//2、创建3个线程对象,模拟3个窗口
Thread window1 = new Thread(ticketWindow2);
Thread window2 = new Thread(ticketWindow2);
Thread window3 = new Thread(ticketWindow2);
//3、给线程对象起名字
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
//4、启动线程
window1.start();
window2.start();
window3.start();
}
}
public class TicketWindow4 implements Runnable{
private int tickets = 500;
private Object object = new Object();
@Override
public void run() {
while (true) {
synchronized (object){
if (tickets > 0) {
//为了模拟更加真实的售票场景,我们加入延迟
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
}
}
}
}
}