- 多线程 的概述:即同时做多件事情;一个服务器可以让多个人同时访问。
- 进程的概述:在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”。
- 在以上图示中,在一个程序中多个线程执行图,看似同时进行,其实是由 CPU 调度,CPU的运行速度很快,所以看起来像是同时执行的。
- 在 java 中提供了实现多线程的两种方式:一种是继承java.lang包下的Thread类,覆写 thread 类的run()方法,在run()方法中实现运行在线程上的代码;另一种是实现 Java .lang.Runnable接口,同样是在run()方法中实现运行在线程上的代码。
- 子类继承Thread类具有多线程能力。
- 启动线程:创建子类对象.start()。
- 具有oop单继承的局限性。
- 分析一下单线程和多线程的运行流程:
- 可以看出,单线程执行时会按照顺序一步步执行,而多线程,在main()方法和run()方法都可以同时运行。
- 线程开启不一定执行,由CPU调度执行。
public class TestThreadP extends Thread{ //继承Thread类
@ Override
public void run() {//重写run()方法
//run方法线程体:
for (int i =; i < 20; i++) {
System.out.println("run方法线程体" + i);
}
}
public static void main(String[] args) {
//创建一个 线程 对象:
TestThreadP thread = new Test Thread P1();
//调用start()方法,开启线程:
thread.start();
//主方法main线程:
for (int i =; i < 2000; i++) {
System.out.println("主方法main线程" + i);
}
}
}
注意 :通过继承Thread类实现了多线程,但是这种方式有一定的局限性。因为Java中只支持单继承,一个类一旦继承了某个父类就无法再继承Thread类。
实现Runnable接口
- 实现接口Runnable具有多线程能力,重写run()方法
- 启动线程:出入目标对象+Thread对象.start()
- 可以避免oop单继承的局限性,方便同一个对象被多个线程使用
与Thread不同,实现Runnable接口,可以解决Java单继承的问题,可以实现多线程,也可以继承于其他的类。
public class TestRunnable implements Runnable {//实现Runnable接口
@Override
//run方法实现体:
public void run() {
for (int i =; i < 2000; i++) {
System.out.println("Runnable 执行!!!" + i);
}
}
public static void main(String[] args) {
//创建runnable接口的实现类:
TestRunnable test = new TestRunnable();
//创建线程对象,通过线程对象来开启线程:
Thread thread = new Thread(test);
//开启线程:
thread.start();
for (int i =; i < 20; i++) {
System.out.println("main 执行!!!" + i);
}
}
}
实现Callable接口
- 当多个线程同时操作同一个资源的情况下,线程不安全,会发生数据紊乱的情况。所以这时候,可以使用实现Callable接口来实现多线程;其可以抛出异常和定义返回值;这部分如果时初学者可以了解一下!到后期还是挺重要的。
- 重写call()方法,实现接口Callable具有多线程能力
- 创建执行服务(添加线程) ExecutorService
- 提交执行线程 (Future)
- 关闭线程 (shutdownNow)
- 可以定义返回值,可以抛出异常。
public class TestCallable implements Callable<Boolean> {//实现Callable接口
private String url;
private String name;
public TestCallable(String url, String name) {
this.url = url;
this.name = name;
}
@Override
public Boolean call(){
WebDownload webdown = new WebDownload2();
webdown.downloader(url,name);
System.out.println("下载文件:" + name);
return true; //返回值
}
}
class WebDownload{
public void downloader(String url,String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//创建目标对象:
TestCallable t = new TestCallable("链接","名字");
TestCallable t = new TestCallable("链接","名字");
TestCallable t = new TestCallable("链接","名字");
//创建执行服务:
ExecutorService ser = Executors.newFixedThreadPool();
//提交执行:
Future< Boolean > r1 = ser.submit(t1);
Future<Boolean> r = ser.submit(t2);
Future<Boolean> r = ser.submit(t3);
//关闭Callable服务
ser.shutdownNow();
}
}
Lambda表达式
- Java中Lambda表达式,虽然说时一种 语法糖 ,但是在多线程中的作用还是非常大的。但是只有一点,如果想要使用 Lambda表达式 ,这个方法必须时函数式接口,不然无法使用Lambda表达式。Lambda表达式的使用让定义函数式接口里的方法更加方便。
- 函数式接口的定义
- 任何接口,如果只包含唯一一个抽象方法,那么就是一个函数式接口。
- 对于函数式接口,可以通过lambda表达式来创建该接口的对象。
public class TestLambda {
public static void main(String[] args) {
Test test = new Testc();
test.run(,"小明");
//Lambda表达式:
test = (a,b) -> { //多个参数可以去掉参数类型,但是必须去掉括号
System.out.println(b + "run--->" + a + "步");
};
test.run(,"小麦");
}
}
interface Test{
void run(int a, String b);
}
class Testc implements Test{
@Override
public void run(int a, String b) {
System.out.println(b + "run--->" + a + "步");
}
}
静态代理模式
静态代理模式,即一个真实对象通过一个代理对象来实现一些具体的方法;一个对象,可以通过例外一个对象来实现某些功能。
- 一个对象,可以通过例外一个对象来实现某些功能;
- 真实对象和代理对象都要实现同一个接口;
public class StaticProxy {
public static void main(String[] args) {
/* residenter rd = null;
rd = ()->{
System.out.println("resident is succeed! == Lambda");
};
rd.resident();*/
new Thread( () -> {
System.out.println("this is lambda!--error");
}).start();
residentc res = new residentc(new You());
res.resident();
}
}
//定义接口:
interface residenter{
void resident();
}
//定义实现类:(真实对象)实现residenter接口
class You implements residenter{
@Override
public void resident() {
System.out.println("resident is succeed!");
}
}
//定义静态代理:(代理对象)实现residenter接口
class residentc implements residenter{
private residenter target;
public residentc(residenter target) {
this.target = target;
}
@Override
public void resident() {
before();
this.target.resident();
atfer();
}
private void before() {
System.out.println("暂无此用户!");
}
private void atfer() {
System.out.println("用户注册成功!");
}
}
线程的生命周期
在Java中,任何对象都有 生命周期 ,线程也不例外,它也有自己的生命周期;线程的生命周期一共包含了五大状态。
线程的状态:(五大状态)
- 新建状态:创建一个线程对象后,该线程对象就处于新建状态。
- 就绪状态:当线程对象调用了start()方法后,该线程就进入就绪状态;等待CPU调度。
- 运行状态:如果处于就绪状态的线程获得了CPU的使用权,并开始执行run()方法中的线程执行体,则该线 程处于运行状态。
- 阻塞状态:一个正在执行的线程在某些特殊情况下,如被人为挂起或执行耗时的输入/输出操作时,会让出CPU的使用权并暂时中止自己的执行,进入阻塞状态。
- 死亡状态:当线程调用stop()方法或run()方法正常执行完毕后,或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。
线程状态观察:对于线程生命周期的观察
public class TestState {
public static void main(String[] args) {
Thread thread = new Thread(()->{
for (int i =; i < 5; i++) {
try {
Thread.sleep();
} catch ( interrupted Exception e) {
e.printStackTrace();
}
}
System.out.println("****");
});
//观察状态:NEW
Thread.State state = thread.getState();
System.out.println(state);
//观察状态:RUNNABLE
thread.start();
state = thread.getState();
System.out.println(state);
//只要线程不终止,就会一直输出:
while (state != Thread.State.TERMINATED){
try {
Thread.sleep();
state = thread.getState();
System.out.println(state);//TIMED_WAITING
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//观察状态:TERMINATED
state = thread.getState();
System.out.println(state);
}
}
对于线程的生命周期,我们只要能够理解线程阻塞的概念、原因就基本没什么问题了。
线程关闭
对于线程中的线程关闭,线程不建议使用自带的stop()和destory()方法,此方法已经被废弃使用了,建议自己定义一个Boolean值,来控制线程的关闭,当这个变量置为false,则终止线程的运行。
public class TestStop implements Runnable {
private boolean flag = true;
private String name;
public TestStop(String name) {
super();
this.name = name;
}
public void stop(){ //定义终止线程的方法
flag = false;
}
@Override
public void run() {
int s =;
while (flag){
System.out.println( name + "输出线程~~~~~" + s++);
}
}
public static void main(String[] args) {
TestStop test = new TestStop("线程A:");
new Thread(test).start(); //开启线程
for (int i =; i < 1000; i++) {
System.out.println("主线程开启:" + i);
}
test.stop(); //调用终止线程的方法,停止线程
System.out.println("线程已停止。");
}
}
线程休眠:sleep()
暂停线程执行常用的方法有sleep();该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。当前线程调用sleep(long millis)方法后,可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。
- sleep(时间)指定当前线程阻塞的毫秒数;
- sleep存在异常interruptedException;
- sleep时间到达后线程进入就绪状态;
- sleep可以模拟网络延迟,倒计时等;
- 每一个对象都有一个锁,sleep不会释放锁;
- 可以放大问题的发生性;
//模拟倒计时:
public class TestSleep{
public static void main(String[] args) {
TestSleep testSleep = new TestSleep();
testSleep.sleep();
}
public void sleep(){
int num =;
while (true){
try {
Thread.sleep();
System.out.println("this is sleep" + num--);
if (num <){
System.out.println("stop!");
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程礼让:yield()
暂停线程执行常用的方法有yield()方法;该方法和sleep()方法有点相似,都可以让当前正在运行的线程暂停,区别在于 yield ()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。即可以让正在运行的线程直接进入就绪状态,让出CPU的使用权。
- 让当前的线程暂停,但是不阻塞;
- 将线程从运行状态转为就绪状态;
- 礼让不是百分百成功的,只是提高优先度的成功几率,是否成功取决于CPU;
//测试礼让线程:
public class TestYield {
public static void main(String[] args) {
MyYield my = new MyYield();
new Thread(my,"a:").start();
new Thread(my,"b:").start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程启动");
Thread.yield(); //线程礼让
System.out.println(Thread.currentThread().getName() + "线程关闭");
}
}
合并线程:Join()
当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行。 线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。
- Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞;
- 类似于生活中的插队;
//测试Join方法:
public class TestJoin implements Runnable{
@Override
public void run() {
for (int i =; i <= 100; i++) {
System.out.println("准备执行Join方法---" + i);
}
}
public static void main(String[] args) {
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
for (int i =; i < 1000; i++) {
if (i ==){
try {
thread.join(); //穿插:在时插入线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
System.out.println("主程序输出---" + i);
}
}
}
}
线程的优先级
- 在应用程序中,如果要对线程进行调度,最直接的方式就是设置线程的优先级。值得的注意的是,优先级越高的线程获得CPU调度的机会越大,而优先级越低的线程获得CPU调度的机会越小。并不是优先级高就一定先调度。
- 线程的优先级:优先级使用数字表示,范围为1~10。
- MIN_PRIORITY: 表示线程最低优先级,值为1;
- NORM_PRIORITY: 表示线程普通优先级,值为5;
- MAX_PRIORITY: 表示线程最高优先级,值为10;
- 下面是获取线程信息的一些基本方法:
//测试线程的优先级:
public class TestPriority {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + "--->" +
Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
Thread t = new Thread(myPriority,"a");
Thread t = new Thread(myPriority,"b");
Thread t = new Thread(myPriority,"c");
//设置优先级:int(-10)优先级越高越大几率被提 前调度
t.start();
t.setPriority(5);
t.start();
t.setPriority(10);
t.start();
}
}
class MyPriority implements Runnable{
@Override
public void run() {
try {
Thread.sleep();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->" +
Thread.currentThread().getPriority());
}
}
守护线程(daemon)
守护线程:将线程设置为守护线程后,只要用户线程执行完毕,守护线程也会随之结束。setDaemon()
- 线程分为用户线程和守护线程;
- 虚拟机必须确保用户线程执行完毕;
- 虚拟机不用等待守护线程执行完毕;
例如:后台记录操作日志,监控内存,垃圾回收等。。。。
//测试线程守护:
public class TestDaemon {
public static void main(String[] args) {
God god = new God();
Thread thread = new Thread(god);
//开启守护线程 setDaemon()--->目前进入无限循环
thread.setDaemon(true); //默认为false,表示用户线程。
thread.start();
You you = new You();
//开启用户线程,只要用户线程结束,守护线程也会随之结束:
new Thread(you).start();
}
}
class God implements Runnable{
//定义一个守护线程,让其一直循环输出:
@Override
public void run() {
while (true){
System.out.println("This is Daemon!");
}
}
}
class You implements Runnable{
//定义一个用户线程,该线程达到一定结果就会结束:
@Override
public void run() {
for (int i =; i <= 36500; i++) {
System.out.println("This is You Pro!");
}
}
}
线程同步与 Lock锁
- 在Java中,多个线程访问一个对象,会发生错误。确保不会出现这种错误,处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。
- 这时候,我们就需要用到“线程同步”。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。
- 当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个用synchronized修的方法中或一个使用synchronized关键字来修饰的代码块中,这个代码块被称作同步代码块。
- 线程同步:(synchronized)形成条件—队列+锁–>保证线程的安全性
- synchronized 方法
- cpublic synchronized void accessVal();
- 被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行该方法
- synchronized块
synchronized(lock)
{
//允许访问控制的代码
}
上面的代码中,lock是一个锁对象,它是同步代码块的关键。当某一个线程执行同步代码块时,其它线程将无法执行当前同步代码块,会发生阻塞,等当前线程执行完同步代码块后,所有的线程开始抢夺线程的执行权,抢到执行权的线程将进入同步代码块,执行其中的代码。
//线程同步的例子
//两个人在银行取同一个账户里面的钱:
public class UnsafeBank {
public static void main(String[] args) {
//账户:
Account account = new Account(,"Cent");
//第一个取款的人:
Drawingmoney you = new Drawingmoney(account,,"You");
//第二个取款的人:
Drawingmoney she = new Drawingmoney(account,,"She");
you.start();
she.start();
}
}
class Account{
int money; //余额
String name; //卡名
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
class Drawingmoney extends Thread{
Account account; //账户
int Dmoney; //取款额度
int Nmoney; //手里额度
public Drawingmoney(Account account, int Dmoney, String name){
super(name);
this.account = account;
this.Dmoney = Dmoney;
}
//取钱
//synchronized 默认锁的是this. 它本身。
@Override
public void run() {
//synchronized代码块:
synchronized (account){
//判断卡里有没有钱:
if (account.money - Dmoney <){
System.out.println(this.getName() + "余额不够,无法取款!");
return;
}
try {
Thread.sleep(); //放大问题的发生性
} catch (InterruptedException e) {
e.printStackTrace();
}
//卡内余额 = 余额 - 取款金额
account.money = account.money - Dmoney;
//手里金额 = 当前金额 + 取款金额
Nmoney = Nmoney + Dmoney;
System.out.println(account.name + "余额为:" + account.money);
System.out.println(this.getName() + "手里金额为:" + Nmoney);
}
}
}
死锁: 多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。因此, 某一个同步块需要同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。
解决方法: 死锁是由于“同步块需要同时持有多个对象锁造成”的,要解决这个问题,就是:同一个代码块,不要同时持有两个对象锁。
//死锁:多个线程互相拥有对方需要的资源,然后形成僵持;
public class SynLock {
public static void main(String[] args) {
//两个对象使用同个资源,造成死锁
Dohomework d = new Dohomework(0,"学生1");
Dohomework d = new Dohomework(0,"学生2");
d.start();
d.start();
}
}
class Pen{
}
class Note{
}
class Dohomework extends Thread{
//需要的资源只有一份,所以使用static
static Pen pen = new Pen();
static Note note = new Note();
int choice; //选择
String student; //人物
Dohomework(int choice, String student){
this.choice = choice;
this.student = student;
}
@Override
public void run() {
try {
dohomewhork();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//互相持有对方的锁,需要拿到对方的资源:
private void dohomewhork() throws InterruptedException {
if (choice == ){
synchronized(pen){
System.out.println(this.student + "获得:pen的锁");
Thread.sleep();
// synchronized (note){ //同步中出现两个锁,会造成死锁
// System.out.println(this.student + "再获得:note的锁");
// }
}
//把另外一个同步锁放在第一个同步锁外面就不会造成死锁:
synchronized (note){
System.out.println(this.student + "再获得:note的锁");
}
}else {
synchronized (note){
System.out.println(this.student + "获得:note的锁");
Thread.sleep();
// synchronized (pen){ //同步中出现两个锁,会造成死锁
// System.out.println(this.student + "再获得:pen的锁");
// }
}
synchronized (pen){
System.out.println(this.student + "再获得:pen的锁");
}
}
}
}
线程并发协作(生产者/消费者模式)
多线程环境下,我们经常需要多个线程的并发和协作;生产者/消费者模式: 生产者——缓冲区——-消费者
- 生产者:负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。
- 消费者:负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)。
- 缓冲区:生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。
- 该模式的好处:
- 有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。
- 生产者不需要和消费者直接打交道,实现了“生产者线程”和“消费者线程”的分离。
- 生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据 。
- 线程并发协作(也叫线程通信),通常用于生产者/消费者模式,常用方法如下:
- (都只能在同步方法或者同步代码块中使用,否则会抛出异常。)
//测试:生产消费者模型---->利用缓冲区
//生产者,消费者,产品,缓冲区
public class TestPC {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Productor(container).start();
new Comsumer(container).start();
}
}
//生产者:
class Productor extends Thread{
SynContainer container;
public Productor(SynContainer container) {
this.container = container;
}
//生产:
@Override
public void run() {
for (int i =; i <= 100; i++) {
container.push(new Pencil(i));
System.out.println("生产了ID为:" + i + "的铅笔");
}
}
}
//消费者:
class Comsumer extends Thread{
SynContainer container;
public Comsumer(SynContainer container) {
this.container = container;
}
//消费:
@Override
public void run() {
for (int i =; i <= 100; i++) {
System.out.println("消费了ID为--->" + container.pop().id + "的铅笔");
}
}
}
//产品:
class Pencil{
int id;
public Pencil(int id) {
this.id = id;
}
}
//缓冲区:
class SynContainer{
//需要一个容器大小:
Pencil[] pencils = new Pencil[];
//定义一个计数器:
int count =;
//生产:
public synchronized void push(Pencil pencil){
while (count == pencils.length){
//产品已满,同之消费者消费,生产者等待。。。。
try {
this.wait(); //让生产者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果没有满就需要丢入产品:
pencils[count] = pencil;
count++;
this.notifyAll(); //解除等待。可以消费了
}
//消费:
public synchronized Pencil pop(){
while (count ==){
//产品已空,通知生产者生产,消费者等待。。。。
try {
this.wait(); //消费者等待。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果可以消费:
count--;
Pencil pencil = pencils[count];
this.notifyAll(); //解除等待,通知生产者生产
return pencil;
}
}
到这里,多线程的基本知识都总结的差不多了,如果想要进一步了解多线程,可以学习JUC编程