目录
进程与线程
多线程是提升程序性能重要方式,也是java中一项重要的技术。多线程是指一个应用程序中有多条并发执行的线索,每条线索都是一个线程,线程之间可以可以交替执行,彼此可以通信。
进程基本概念及理解
程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
进程则是一个动态的概念,进程则是执行程序的一次执行过程,是系统进行资源分配和调度的基本单位。
- 程序就是保存在磁盘上,编译好的二进制文件,不占用系统资源
- 进程存在于内存中,占用系统资源
- 一个单核的CPU同一时刻只能处理一个进程
在单核CPU背景下,平时我们在一台计算机上,可以同时打游戏,听音乐,语音通话可以同时进行,感觉是有多个进程在运行,但实际上是计算机系统采用了多到程序设计技术,就是在内存管控下,多个进程之间相互穿插执行。
采用多到程序设计的系统会把CPU周期划分为多个长度相同的时间片,在每个时间片内只能处理一个进程,也就是说内存里面的多个进程轮流交替使用CPU。而我们产生错觉的原因是,CPU的时间片非常小,我们难以感受到其中差异。因此在宏观上,可以认为是计算机可以并发执行多个进程。
线程基本概念及理解
每个运行的程序就是以一个进程,一个进程里面可以有多个执行单元同时执行,这些执行单元就是线程,每个进程至少有一个线程。线程是CPU调度和执行的的单位。
多线程和单线程的区别:
多线程看起是同时执行的,他们和进程是一样的,由CPU轮流执行,只是CPU执行速度很快,几乎感觉不到。
真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉
进程线程对比
- 进程是系统进行资源分配和调度的基本单位,线程是CPU调度和执行的的单位。
- 一个进程里面至少包含一个多线程,可以包含多个。
关于多线程理解:
- 线程就是独立的执行路径;
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
- main() 称之为主线程,为系统的入口,用于执行整个程序;
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为的干预的。
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
- 线程会带来额外的开销,如cpu调度时间,并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
创建进程
三种创建进程方式:
继承Thread类创建多线程
通过继承Thread类,重写类中run方法,即可实现多线程。
start() 启动新线程,之后Java虚拟机自动调用run方法。
package Example1;
// 继承Thread类创建多线程
public class Test1 {
public static void main(String[] args) {
Mythread mythread = new Mythread();
mythread.start();
while(true) {
System.out.println("main()方法在运行");
}
}
}
class Mythread extends Thread {
public void run() {
while(true) {
System.out.println("MyThread类的run方法在运行");
}
}
}
运行结果:
Mythread.start()启动新的线程,和之前main()线程里的代码同时执行。
实现Runnable接口创建多线程
Thread类提供了一个构造方法,Thread(Runnable target) ,参数是一个接口,它只有一个run方法。
通过这个构造方法创造Thread对象时,需要传递一个Runnable实现接口对象。通过Thread的对象调用start()方法,来执行run()。
这样一来,调用新线程任务就交给Thread执行,原本的类只需要实现Runnable接口即可。可以避免Java单继承的问题。
package Example2;
public class Test2 {
public static void main(String[] args) {
Mythread mythread = new Mythread();
Thread thread = new Thread(mythread);
thread.start();
while(true) {
System.out.println("main()方法在运行");
}
}
}
// Mythread实现Runnable()接口,并且重写run()方法
class Mythread implements Runnable {
public void run() {
while(true) {
System.out.println("MyThread类的run方法在运行");
}
}
}
运行结果:
实现Callable接口创建多线程
前两种创建进程方法存在缺点,无法从run()方法获取返回值。
实现Callable接口可以满足创建多线程和获取返回值的要求。
package Example3;
import java.util.concurrent.*;
// 实现Callable创建多线程
public class Test3 {
public static void main(String[] args) throws InterruptedException,ExecutionException{
// 创建实现Callable接口的实例对象
Mythread mythread = new Mythread();
// 使用FutureTask封装Mythread类
FutureTask<Object> ft = new FutureTask<>(mythread);
// 使用Thread(Runnable target,String name)创建线程对象
Thread thread = new Thread(ft,"thread");
// 启动新线程
thread.start();
// currentThread().getName() 当前线程名字 get() 返回管理值
System.out.println(Thread.currentThread().getName()+"的返回结果"+ft.get());
int a=0;
while(a++<5) {
System.out.println("main()方法在运行");
}
}
}
// 实现Callable接口
class Mythread implements Callable<Object> {
public Object call() throws Exception {
int i = 0;
while (i++ < 5) {
System.out.println(Thread.currentThread().getName() + "的call()方法在运行");
}
return i;
}
}
Thread类与Runnable接口实现多线程对比
1. Thread类实现多线程时通过结成Thread的方式实现的,而Runnable接口实现的方式可以解决Java单继承的问题。
2. Thread类实现多线程无法资源共享,而Runnable接口实现却可以做到
package Example4;
public class Test4 {
public static void main(String[] args) {
new TicketWindows().start(); // Thread-0
new TicketWindows().start(); // Thread-1
new TicketWindows().start(); // Thread-2
new TicketWindows().start(); // Thread-3
}
}
class TicketWindows extends Thread {
private int tickets = 100;
public void run() {
while(tickets>0) {
// 调用Thread类种的currentThread().get()获取当前执行线程的名字
Thread th = Thread.currentThread(); //返回一个对象
String name = th.getName();
System.out.println(name+"正在发售第" + tickets-- + "张票");
}
}
}
package Example5;
public class Test5 {
public static void main(String[] args) {
TicketWindow tk = new TicketWindow();
new Thread(tk,"窗口1").start();
new Thread(tk,"窗口2").start();
new Thread(tk,"窗口3").start();
new Thread(tk,"窗口4").start();
}
}
class TicketWindow implements Runnable {
private int tickets=100;
public void run () {
while(tickets>0) {
Thread th =Thread.currentThread();
String name = th.getName();
System.out.println(name + "正在发售第" + tickets-- + "张票");
}
}
}
综合考虑,建议在创建多线陈的时候使用实现Runnable方式创建多线程。
线程状态
线程的基本状态:新建,运行,阻塞,等待,中止
线程调度
线程调度有两种模型:分时调度模型和抢占式调度模型
- 分时调度模型:所有线程轮流获得CPU的使用权,就是平均主义
- 抢占式调度模型:根据线程优先级获得CPU使用权,优先级相同则线程随机获得CPU使用权
Java虚拟机默认采用抢占式调度模型
线程的优先级
优先级高的的线程获得CPU使用权的机会就越大,优先级低的线程获得CPU使用机会越小。
线程的优先级用1-10,数字越大,优先级越高。Thread类中提供了三个静态常量表示线程的优先级。
static int MAX_PRIORITY:表示线程最高优先级,表示10;
static int MIN_PRIORITY :表示线程最低级,表示1;
static int NORM_PRIORITY:表示线程默认优先级,表示5;
package Example6;
public class Test6 {
public static void main(String[] args) {
Thread mp = new Thread(new MaxPriority(),"优先级较高线程");
Thread np = new Thread(new MinPriority(),"优先级较低线程");
mp.setPriority(Thread.MAX_PRIORITY);
np.setPriority(Thread.MIN_PRIORITY);
mp.start();
np.start();
}
}
class MaxPriority implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"正在输出"+i);
}
}
}
class MinPriority implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"正在输出"+i);
}
}
}
其实,即使设置了线程的优先级,一样无法确保这个线程一定先执行,因为它有很大的随机性。它并无法控制执行哪个线程,因为线程的执行,是抢占资源后才能执行的操作,而抢点资源时,最多是给于线程优先级较高的线程一点机会而已,能不能抓住可是不一定的。。
线程休眠
是指让当前线程暂停执行,从运行状态进入阻塞状态,将CPU资源让给其它线程的调度方式
package Example7;
public class Test7 {
public static void main(String[] args) throws Exception {
new Thread(new SleepThread()).start();
for(int i=1;i<=8;i++){
if(i==5){
Thread.sleep(2000); // 这是一个静态方法,只能控制当前的进程,不能控制其它进程
}
System.out.println("主线程正在输出:"+i);
Thread.sleep(500);
}
}
}
class SleepThread implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 8; i++) {
if(i==3) {
try{
Thread.sleep(2000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("SleepThread线程正在输出:"+i);
try{
Thread.sleep(500);
}catch (Exception e) {
e.printStackTrace();
}
}
}
}
这是一个静态方法,只能控制当前的进程,不能控制其它进程
线程插队
线程插队指暂停当前线程,转而之心跟另外一个线程,原来线程进入阻塞状态,直到但钱线程执行完毕,在执行原来的线程。
package Example8;
public class Test8 {
public static void main(String[] args) throws InterruptedException{
Thread th = new Thread(new JoinRunnable(),"thread");
th.start();
for(int i=1;i<=5;i++){
System.out.println(Thread.currentThread().getName()+"输出:"+i);
if(i==2) {
th.join();
}
}
}
}
class JoinRunnable implements Runnable {
@Override
public void run() {
for(int i=1;i<=3;i++){
System.out.println(Thread.currentThread().getName()+"输出:"+i);
}
}
}
Thread还提供了带有时间参数的线程插队方法,必须等待插入时间过后,其它线程才能执行,而且时间过后,其它线程都可以与当前线程争夺CPU资源。
package Example9;
public class Test9 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new JoinRunnable(),"thread");
thread.start();
for(int i=1;i<=5;i++){
System.out.println(Thread.currentThread().getName()+"输出:"+i);
if(i==2){
thread.join(3000);
}
}
}
}
class JoinRunnable implements Runnable {
@Override
public void run() {
for(int i=1;i<=3;i++){
try{
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"输出:"+i);
}
}
}
线程让步
线程让步是指在某个时间点,当前线程从就绪状态或执行状态转到阻塞状态,将CPU资源让给其它线程使用。
package Example10;
public class Test10 {
public static void main(String[] args) {
Thread t1 = new YieldThread("t1");
Thread t2 = new YieldThread("t2");
t1.start();
t2.start();
}
}
class YieldThread extends Thread {
public YieldThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"---"+i);
if(i==2){
System.out.println("线程让步:");
Thread.yield();
}
}
}
}
yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!
线程中断
是指在线程执行过程中通过手动操作停止线程。例如:用户执行一次操作,因为网络问题导致延迟,这个线程就一直处于运行状态。如果用户幼结束这个操作,就需要线程中断机制。
interrupt():中断当前进程
isInterrupt():判断当前进程状态 ture-已中断 false-未中断
package Example11;
public class Test11 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if(i==5){
Thread.currentThread().interrupt();
System.out.println("thread线程是否已中断---"+Thread.currentThread().isInterrupted());
}
}
}
});
thread.start();
}
}
守护线程
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕,比如:main();
- 虚拟机不用等待守护线程执行完毕
package Example12;
public class Test12 {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread thread = new Thread(god);
thread.setDaemon(true); // 设置守护进程,true为守护线程,false默认用户线程
thread.start();
new Thread(you).start();
}
}
class God implements Runnable {
@Override
public void run() {
while(true) {
System.out.println("上帝保佑你");
}
}
}
class You implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("你还活着");
}
System.out.println("Goodbye World!");
}
}
用户进程结束后,守护进程就会结束。
线程同步
多线程的并发执行可以提高程序的效率。多个线程共享资源时,会引发安全问题。
同步代码块
线程安全问题其实就是由多个线程同时处理共享资源所导致的。要想解决这个问题,就必须保证在任何时刻都只能有一个线程访问共享资源。
Java中提供了同步机制,当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字修饰的代码块,这个代码块被称为同步代码块。
当某个线程执行当前同步代码块时,其它线程无法执行同步代码块,进入阻塞状态。当前线程执行完同步代码块后,再与其它线程重新抢夺CPU的执行权。
同步代码块
package Example14;
public class Test14 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket,"线程1").start();
new Thread(ticket,"线程2").start();
}
}
class Ticket implements Runnable {
private int tickets=100;
Object lock = new Object(); // 锁对象的创建不能放在run()方法里面,它是锁的唯一标志
@Override
public void run() {
while(true) {
synchronized (lock) {
try{
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(tickets>0) {
System.out.println(Thread.currentThread().getName()+"---卖出的票"+tickets--);
}else {
break;
}
}
}
}
}
同步方法
synchronized还可以修饰方法,同步方法和同步代码块作用一样。
package Example15;
public class Test15 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket,"线程1").start();
new Thread(ticket,"线程2").start();
}
}
class Ticket implements Runnable {
private int tickets=100;
@Override
public void run() {
while(true) {
saleTicket();
if(tickets<=0) {
break;
}
}
}
private synchronized void saleTicket(){
if(tickets>0) {
try{
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"---卖出的票"+tickets--);
}
}
}
同步代码块的锁是自己定义的锁对象。同步方法的锁是this,即当前调用方法的线程。确保了锁的唯一性。
在多线程竞争下 , 加锁 , 释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题
死锁问题
多个线程各自占有一些共享资源 , 并且互相等待其他线程占有的资源才能运行 , 而 导致两个或者多个线程都在等待对方释放资源 , 都停止执行的情形 . 某一个同步块 同时拥有 “ 两个以上对象的锁 ” 时 , 就可能会发生 死锁问题。
产生死锁问题的原因:
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件 : 进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件 : 若干进程之间形成一种头尾相接的循环等待资源关系。
只要能破解其中任何一个条件,就能解开死锁。
Lock锁
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对 象来实现同步。同步锁使用Lock对象充当
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开 始访问共享资源之前应先获得Lock对象
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语 义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
package Example16;
import java.util.concurrent.locks.ReentrantLock;
public class Test16 {
public static void main(String[] args) {
TestLock tk = new TestLock();
new Thread(tk).start();
new Thread(tk).start();
new Thread(tk).start();
new Thread(tk).start();
}
}
class TestLock implements Runnable {
int tickets = 10;
// 定义lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true) {
try{
lock.lock();// 加锁
if(tickets>0){
try {
Thread.sleep(1000);
} catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(tickets--);
}else {
break;
}
} finally {
lock.unlock(); // 解锁
}
}
}
}
synchronized与lock对比:
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了 作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展 性(提供更多的子类)
到此多线程的学习先告一段落了,还差一个经典的问题:生产者消费者,需要解决线程通信的问题,后续会补上的。