一、基本概念
- 程序(program) :是为了完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
- 进程(process):是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有他自身的产生、存在和消亡的过程。—生命周期
>如:运行中的qq…
>程序是静态的,进程是动态的
>进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域. - 线程(thread) :进程可以进一步细分为线程,是一个程序内部的一条执行路径.
>如果一个进程同一时间执行多个线程,就是支持多线程的
>线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器,线程切换的开销小
>一个进程中的多个线程共享相同的内存单元/内存地址空间,他们从同一堆中分配对象,可以访问相同的变量和对象.这就使得线程通信更简洁,高效,但是,多个线程共享同一资源可能会造成安全隐患. - 单核cpu和多核cpu的理解:
>单核:其实是一种假的多线程,因为在一个时间单元内,只能执行一个线程的任务,但是因为cpu时间特别短,因此一般感觉不出来.
>如果是多核,才能更好的发挥多线程的效率.
>一个就Java程序至少包含三个线程:Java主线程,gc,异常处理线程. - 并行与并发:
>并行:多个cpu同时执行多个任务,多个人做不同的事情.
>并发:一个cpu(时间片)同时执行多个任务.比如:秒杀,多个人同事做意见事情. - 使用多线程的优点:背景:以单核cpu为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程用的时间短(因为cpu中切换线程需要耗时),那为何仍然需要多线程呢?
多线程程序的优点:
1)提高应用程序的响应,对于图形化界面更有意义,增强用户的体验.
2)提高计算机cpu的利用率
3)改善程序的结构,将即长有复杂的进程分为多个线程,独立运行,利于理解和修改. - 核实需要用到多线程
1)程序需要执行两个或多个任务.
2)程序需要实现一些需要等待的任务时,如:用户输入、文件读写操作、网络操作。
3)需要一些后台运行的程序时。
二、线程的创建和使用
2.1多线程的创建
一、方式一:继承thread类
1)创建一个继承于thread类的子类
2)重写thread类中的run()–>将此线程执行的操作声明在run()中
3)创建thread类的子类对象
4)通过此子类对象调用start():启动当前线程,调用当前线程的run().
问题一:不能通过调用run()方法来启动线程.
问题二:一个已经拿启动的线程不能再次通过start方法来调用,会报异常:IllegalThreadStateException
thread买票:
package tickets;
/**
* @author TR
* @date 2020/7/29 - 下午 5:16
* @describe:
*/
public class TicketsTest2 {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.start();w1.setName("窗口一");
w2.start();w2.setName("窗口二");
w3.start();w3.setName("窗口三");
}
}
class Window extends Thread {
private static int num = 100;
@Override
public void run() {
if (num > 0) {
for (int i = 0; i <= 100; i++) {
num--;
if (num < 0) {System.out.println("票额不足!");
break;
}
System.out.println(getName()+"售票成功!剩余票数为:" + num);
}
}
}
}
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}.start();
如果只用一次的线程,可以考虑上面的创建方式.
二、’方式二:实现runnable接口
1)创建一个实现runnable接口的类
2)实现类去实现runnable接口中的抽象方法:run()
3)创建实现类的对象
4)将此对象作为参数传递到Thread类的构造器中,创建thread类的对象
5)通过thread类的对象调用start方法.
Runnable买票(三个线程共用一个Tickets对象,所以不用加static
)
package tickets;
/**
* @author TR
* @date 2020/7/29 - 下午 7:06
* @describe:
*/
public class TicketNumbers3 {
public static void main(String[] args) {
Tickets t1 = new Tickets();
Thread tt1 = new Thread(t1);
Thread tt2 = new Thread(t1);
Thread tt3 = new Thread(t1);
tt1.setName("窗口一");
tt2.setName("窗口二");
tt3.setName("窗口三");
tt1.start();
tt2.start();
tt3.start();
}
}
class Tickets implements Runnable {
public int num = 100;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "取出了1票,剩余:" + num);
num--;
}
}
}
}
三、两种方式的对比
开发中:优先选择实现runnable接口的方式
原因:1、实现的方式没有类的单继承的局限性
2、实现的方式更适合来处理多个线程有共 享的数据的情况。因为实现类只创建了一个
联系:Thread也是实现了Runnable接口的一个实现类。
相同点:两种方式都需要重写run(),将线程要执行的代码声明到run()中。
三、线程中常见的方法
1)start():启动当前线程:调用当前线程的run();
2)run():通常重写Thread中的方法,将创建的线程要执行的操作声明在此方法中.
3)currentThread():静态方法返回执行当前代码的线程.
4)getName():获取当前线程的名字;
5)setName ():设置当前线程的名字
6)yield():释放当前cpu的执行权.
7)join():在线程a中调用线程b的 join(),此时线程a就进入阻塞状态,直到线程b完全执行以后,线程a才结束阻塞状态.
8)stop():已过时,当执行此方法时,强制结束当前线程.
9)sleep(long milliontime):让当前线程"睡眠"指定的毫秒数,在指定的毫秒时间内,当前线程是阻塞状态.注意:此方法有异常,且只能try-catch,因为在重写之前的run方法中,并没有抛出异常,作为重写的子类,所抛出的异常只能小于等于父类中声明的异常.
10)isAlive():判断当前线程是否存活.
四、线程的优先级
1)MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5(默认优先级)
2)如何获取和设置当前线程的优先级:
>getPriority():获取当前线程的优先级
>setPriority(int p):设置线程的优先级
说明:高优先级的线程要抢占低优先级的线程cpu的执行权.但是只从效率上讲,高优先级的线程高概率下被执行.但是并不意味着只有当高优先级的线程执行完之后,低优先级的线程才执行.
五、线程的安全问题
方式Ⅰ 、同步代码块
语法:synchronized(同步监视器){需要被同步的代码}
说明:1、操作共享数据的代码,即为需要被同步的代码。(包含的代码要恰到好处)
2、共享数据:多个线程共同操作的变量。比如购票中的tickets。
3、同步监视器,俗称:锁。任何一个类的对象。都可以充当锁。要求:多个线程必须共用同一把锁。
补充:在实现runnable接口创建多个线程的时候,可以考虑使用this来充当同步监视器.thread不能用的原因是采用thread创建线程时,创建了多个对象,this不唯一.
方式Ⅱ、同步方法
使用synchronized同步的方式,优点:解决了线程安全的问题。
缺点:操作同步代码时,只能有一个线程参与,其他线程等待,相当于是一个线程的过程,效率低。
关于同步方法的总结:
1)同步一方法任然涉及到同步监视器,只是不需要我们显示的去声明
2)非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身.
六、线程死锁问题及其解决方案
1)死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现了死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
2)解决方案
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
- 使用lock锁
(lock锁代码演示)
package threadsafe;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author TR
* @date 2020/8/5 - 下午 4:05
* @describe:
*/
public class LockTest {
public static void main(String[] args) {
Tickets t = new Tickets();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
class Tickets implements Runnable {
static int num = 100;
private ReentrantLock lock = new ReentrantLock(true);//创建锁对象
@Override
public void run() {
while (true) {
try {
lock.lock();//加锁
if (num > 0) {
System.out.println(Thread.currentThread().getName() + " 抢到了一票" + "票号为: " + num);
num--;
} else {
System.out.println("票数不足!");
break;
}
} finally {
lock.unlock();//释放锁
}
}
}
}
synchronized与lock的对比
1)Lock是显示锁(手动开启和关闭锁,别忘记关闭锁),sychronized是隐式锁,出了作用域自动释放.
2)Lock只有代码块锁,synchronized有代码块锁和方法锁
3)使用lock锁居民将花费较少的时间来调度线程.并且具有更好的扩展性(提供更多的子类)
优先使用顺序:Lock–>同步代码块(已经进入方法体,分配相应的资源)–>同步方法(在方法体之外)
七、线程间的通信
涉及到的三个方法:
- wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器.
- notify():一旦执行此方法,就会唤醒被wait的一个线程.如果有多个线程被wait,就唤醒优先级较高的线程.
- notifyAll():一旦被执行,就会唤醒所有被wait线程.
说明
- 这三个方法必须使用在同步代码块或者同步方法中.
- 三个方法的调用者必须是同步代码块或同步方法中的监视器,否则,会出现异常.
- 这三个方法都是定义在Java.lang.Object下
- 代码展示
package threadsafe;
/**
* @author TR
* @date 2020/8/6 - 下午 2:49
* @describe: 两个人买票, 你买一下, 我买一下
*/
public class Communication {
public static void main(String[] args) {
Print p = new Print();
Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
t1.setName("客户1");
t2.setName("客户2");
t1.start();
t2.start();
}
}
class Print implements Runnable {
private int tickets = 100;
Object obj=new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
obj.notify();
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "取到了一票!剩余为:" + tickets);
tickets--;
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("票额不足!");
break;
}
}
}
}
}
sleep和wait的异同
- 相同点:一旦执行都可以使得当前线程进入阻塞状态.
- 不同点
1)两个方法声明的位置不同,Thread类中声明sleep(),Object类中声明wait().
2)调用的方法不同:sleep()可以在任何需要的场景下调用.,wait()必须使用在同步代码块中.
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或者同步方法中,sleep()不会释放锁,而wait()会释放.
八、线程创建的另外两种方式
1)实现callable接口—JDK5新增
步骤:
- 创建一个实现runnable接口的实现类。
- 实现call方法,将此线程需要执行的代码声明在call()中
- 创建callable实现类的对象
- 将此实现类的对象作为参数传到FutherTask的构造器中,创建FutherTask对象
- 将FutherTask的对象作为参数传递到thread类的构造器中,并调用start().
- 获取callable中call的返回值
- get()返回值就是构造器参数实现call方法的返回值.
ps:如何理解实现callable接口比实现runnable接口更加强大?
a)call()可以有返回值
b)call()可以抛出异常,被外面的操作捕获,获取异常的信息.
c)call()支持泛型.
代码展示:
package callableTest;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author TR
* @date 2020/8/8 - 下午 12:15
* @describe:
*/
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
Sum s = new Sum();
Sum1 s1 = new Sum1();
Sum3 s2 = new Sum3();
FutureTask f = new FutureTask(s);
FutureTask f1 = new FutureTask(s1);
FutureTask f2 = new FutureTask(s2);
new Thread(f).start();
new Thread(f1).start();
new Thread(f2).start();
Object o = f.get();//得到call方法的返回值
Object o1 = f1.get();//得到call方法的返回值
Object o2 = f2.get();//得到call方法的返回值
System.out.println("1~100的和:" + o);
System.out.println("1~100的偶数和:" + o1);
System.out.println("1~100的奇数和:" + o2);
long end = System.currentTimeMillis();
System.out.println("多线程耗时" + (end - start));
}
}
class Sum implements Callable {
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}
class Sum1 implements Callable {
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
sum += i;
}
}
return sum;
}
}
class Sum3 implements Callable {
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 != 0) {
sum += i;
}
}
return sum;
}
}
2)线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可避免频繁创建、销毁。
好处:1)提高相应速度(减少了创建线程的时间).
2)降低资源的消耗(重复利用线程池中的线程,不必每次都创建)
3)便于线程管理
代码展示
package threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @author TR
* @date 2020/8/8 - 上午 11:38
* @describe:
*/
public class ThreadPool {
public static void main(String[] args) {
//提供指定数量的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) executorService;
//设置线程池的属性
int corePoolSize = service1.getCorePoolSize();//核心池的大小
int maximumPoolSize = service1.getMaximumPoolSize();//最大线程数
//执行指定的线程的操作.需要提供实现Runnable接口或者Callable接口的实现类
executorService.execute(new Number());
executorService.execute(new Number1());
//关闭线程池
executorService.shutdown();
}
}
class Number implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}
class Number1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
}