多线程
程序和进程的概念------------------------------
程序 - 数据结构 + 算法,主要指存放在硬盘上的可执行文件
进程 - 主要指运行在内存中的可执行文件
目前主流的操作系统都支持多进程,为了让操作系统同时可以执行多个任务
但进程是重量级的,也就是新建一个进程会消耗CPU和内存空间等系统资源,因此进程的数量比较局限
通常一个进程包含一个或者多个线程,而一个进程可能占用一个或者多个端口
但一个端口基本只能由一个进程占用(如果以后说明一个进程基本占用一个端口,那么就是这个意思,虽然可以占用多个)
要注意:只有与网络有关的进程才需要占用端口号,因为端口只是操作网络的而已,对于内部的其他操作,进程并非一定操作端口哦,反正是线程组(当然,进程也包含其他东西,如资源分配操作(因为我们可以继续创建线程),所以也不只是线程组,这也是为什么我们大多会称为进程,而不是线程组的原因)
线程的概念------------------------------
为了解决上述问题就提出线程的概念,线程就是进程内部的程序流
也就是说操作系统内部支持多进程的,而每个进程的内部又是支持多线程的
线程是轻量的,新建线程会共享所在进程的系统资源(如CPU和内存空间),也就是说一个进程的总资源是有限的,一般是该进程获取的(具体如何获取可以百度,这不是java要做的事情),因此目前主流的开发都是采用多线程
多线程是采用时间片轮转法来保证多个线程的并发执行,所谓并发就是指宏观并行微观串行的机制(单核来说)
对于单核CUP来说:多个线程就是看起来是一起运行的,但是实际上没有一起运行
因为运行速度太快,看不出来(仔细一看不是一起运行的,粗略一看是一起运行的)
对于多核CUP来说(一个cpu可以有多个核心,各个核心可以操作线程,也可以存在多个cpu):多个线程若分配与两个或者两个以上的CPU同时运行,一般核心也算,那么就有可能真正的同时运行
如两个线程分别由各自的CPU来操作,只是占资源,操作由CPU操作,一般我们以多核为主,因为现在的电脑基本上都是多核
对于进程来说,我们通常在进程里运行多个线程,且线程占用的CPU和内存空间资源都是由进程的CPU和内存空间资源分配的
而进程的CPU和内存资源都是由系统资源分配,如一些电脑的内存空间大小有8G或者16G内存
而CPU资源也要看型号或者主频,外频,总线频率
线程的创建------------------------------
public class Thread
extends Object
implements Runnable
java.lang.Thread类代表线程,任何线程对象都是Thread类(子类)的实例
Thread类是线程的模板,封装了复杂的线程开启等操作,封装了操作系统的差异性
创建方式------------------------------
自定义类继承Thread类并重写run方法,然后创建该类的对象调用start方法
自定义类实现Runnable接口并重写run方法,创建该类的对象作为实参来构造Thread类型的对象
然后使用Thread类型的对象调用start方法
相关的方法------------------------------
Thread(),使用无参的方式构造对象
Thread(String name),根据参数指定的名称来构造对象
Thread(Runnable target),根据参数指定的引用来构造对象,其中Runnable是个接口类型
Thread(Runnable target,String name),根据参数指定引用和名称来构造对象
void run(),若使用Runnable引用构造了线程对象,调用该方法时最终调用接口中的版本
若没有使用Runnable引用构造线程对象,调用该方法时则啥也不做
void start(),用于启动线程,Java虚拟机会自动调用该线程的run方法
package com.lagou.task18;
public class ThreadTest {
public static void main(String[] args) {
Thread t1 = new Thread();
Thread t2 = new Thread("1");
t1.run();
System.out.println("我想看看你到底是否真的啥也不干!");
}
}
执行流程------------------------------
执行main方法的线程叫做主线程,执行run方法的线程叫做新线程/子线程
main方法是程序的入口,对于start方法之前的代码来说,由主线程执行一次,当start方法调用成功后线程的个数由1个变成了2个
新启动的线程去执行run方法的代码,主线程继续向下执行,两个线程各自独立运行互不影响,当run方法执行完毕后子线程结束
当main方法执行完毕后主线程结束,一般两个线程(一般不包括主线程)执行没有明确的先后执行次序,由操作系统调度算法来决定
可以看作谁先用CPU执行,即也可以认为是随机的,这里之所以不包括主线程,因为主线程必然是先占用cpu的,就相当于在线程里进行操作创建线程,而不是同时进行操作,所以主线程基本会优先
即对于单核来说:虽然在main里面创建的线程,但还会与main主线程抢占CPU资源,即谁先抢占(操作系统调度算法),谁先执行
不会因为该线程是主线程的子线程而不进行抢夺
如:它先分时间片给第一个调用,然后时间到了,把第一个踢出去
再把时间片分给第二个调用,时间到了又把第二个踢出去
又让第一个来,就好像2个人排队玩游戏,一人5分钟
即时间片是先分配好的,然后再继续分配,通常叫做抢占,即它通常会依次运行,若你运行时,程序有输出多条的
基本上是多核或者程序运行时间的差异,最有可能是优先级的关系(优先级大分配的时间多,即一般总体可以运行的时间久)
若还是会有一个输出两次的,则应该是内部运行时间的差异
那么就有可能会依次运行(程序时间不相差过大,而多核在相差不多时,会出现不同结果,而单核不会,分配到同一个CPU里面,微观上是依次执行)
之所以是有可能,是因为程序运行时间的不同,而导致某个程序比其他程序多运行几次,然后他才运行完,但时间是一样的
但对于多核来说:就会同时运行了,但一开始还是会先占CPU的先运行(注意:一般不是抢占对方的,极少数情况下,与单核一样,抢占同一个,或者所有的CPU都有线程了,那么在对应同一个CPU下,可以认为是单核了,该cpu可以指核心或者不同cpu),只不过之后不会再依次运行,因为由不同CPU操作了(不同的核,可以称为不同的CPU),也就是说,多核情况下,一个线程占用一个核心,如果核心够多,其他核心是没有被使用的
package com.lagou.task18;
public class SubThreadRun extends Thread {
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
System.out.println("run方法中:i = " + i);
}
}
}
package com.lagou.task18;
public class SubThreadRunTest {
public static void main(String[] args) {
Thread t1 = new SubThreadRun();
t1.start();
for (int i = 1; i <= 20; i++) {
System.out.println("-----------------main方法中:i = " + i);
}
}
}
package com.lagou.task18;
public class SubRunnableRun implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
System.out.println("run方法中:i = " + i);
}
}
}
package com.lagou.task18;
public class SubRunnableRunTest {
public static void main(String[] args) {
SubRunnableRun srr = new SubRunnableRun();
Thread t1 = new Thread(srr);
t1.start();
for (int i = 1; i <= 20; i++) {
System.out.println("-----------------main方法中:i = " + i);
}
}
}
方式的比较------------------------------
继承Thread类的方式代码简单,但是若该类继承Thread类后则无法继承其它类
而实现Runnable接口的方式代码复杂,但不影响该类继承其它类以及实现其它接口,因此以后的开发中推荐使用第二种方式
匿名内部类的方式------------------------------
使用匿名内部类的方式来创建和启动线程
package com.lagou.task18;
public class ThreadNoNameTest {
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
System.out.println("张三说:在吗?");
}
}.start();
Runnable ra = new Runnable() {
@Override
public void run() {
System.out.println("李四说:不在。");
}
};
Thread t2 = new Thread(ra);
t2.start();
new Thread(()-> System.out.println("李四说:不在。")).start();
}
}
线程的生命周期------------------------------
![26-多线程_第1张图片](http://img.e-com-net.com/image/info8/458500b332c64fccb5f948602ba3003d.jpg)
新建状态 - 使用new关键字创建之后进入的状态,此时线程并没有开始执行
就绪状态 - 调用start方法后进入的状态,此时线程还是没有开始执行(或者说还没有准备执行run方法)
运行状态 - 使用线程调度器调用该线程后进入的状态,此时线程开始执行
当线程的时间片执行完毕后,任务没有完成时,回到就绪状态((可以认为是两个线程或者多个线程互相抢夺时间片,注意这个时间片只是一开始的,即与开始的线程进行抢夺,之后就不考虑时间片了,除非再次的抢)
消亡状态 - 当线程的任务执行完成后进入的状态,此时线程已经终止
阻塞状态 - 当线程执行的过程中发生了阻塞事件进入的状态,如:sleep方法。阻塞状态解除后进入就绪状态
即不管怎样,无论你是阻塞还是没有在运行完,最后都要重新在运行的地方开始,即就绪状态,但并没有清除你以前的行为
且创建时,只要是关于Thread类的,即调用了他的构造方法,基本上就是创建一个线程
注意:若直接输出直接量,且放在开启线程(start方法)后面,基本上就会先运行该输出的值,因为你在运行run方法时
需要输出的时间远远大于直接输出直接量,所以通常会发现你run在运行时,还没输出数值时,他就输出了(多核来说,单核也会)
实际上是他快
但对于单核,则有可能会依次运行(程序时间不相差过大,而多核在相差不多时,会出现不同结果,而单核不会,分配到同一个CPU里面,微观上是依次执行)
之所以是有可能,是因为程序运行时间的不同,而导致某个程序比其他程序多运行几次,然后他才运行完,但时间是一样的
线程的编号和名称------------------------------
long getId(),获取调用对象所表示线程的编号
String getName(),获取调用对象所表示线程的名称
void setName(String name),设置/修改线程的名称为参数指定的数值
static Thread currentThread(),获取当前正在执行线程的引用
案例题目
自定义类继承Thread类并重写run方法,在run方法中先打印当前线程的编号和名称
然后将线程的名称修改为"zhangfei"后再次打印编号和名称。要求在main方法中也要打印主线程的编号和名称
package com.lagou.task18;
public class ThreadIdNameTest extends Thread {
public ThreadIdNameTest(String name) {
super(name);
}
@Override
public void run() {
System.out.println("子线程的编号是:" + getId() + ",名称是:" + getName());
setName("zhangfei");
System.out.println("修改后子线程的编号是:" + getId() + ",名称是:" + getName());
}
public static void main(String[] args) {
ThreadIdNameTest tint = new ThreadIdNameTest("guanyu");
tint.start();
Thread t1 = Thread.currentThread();
System.out.println("主线程的编号是:" + t1.getId() + ", 名称是:" + t1.getName());
System.out.println(System.currentTimeMillis());
}
}
package com.lagou.task18;
public class RunnableIdNameTest implements Runnable {
@Override
public void run() {
Thread t1 = Thread.currentThread();
System.out.println("子线程的编号是:" + t1.getId() + ", 名称是:" + t1.getName());
t1.setName("zhangfei");
System.out.println("修改后子线程的编号是:" + t1.getId() + ", 名称是:" + t1.getName());
}
public static void main(String[] args) {
RunnableIdNameTest rint = new RunnableIdNameTest();
Thread t2 = new Thread(rint, "guanyu");
t2.start();
Thread t1 = Thread.currentThread();
System.out.println("主线程的编号是:" + t1.getId() + ", 名称是:" + t1.getName());
}
}
常用的方法------------------------------
static void yield(),当前线程让出处理器(离开Running状态),使当前线程进入Runnable状态等待,不占用CPU资源(相当于我直接退出重新抢夺时间片,但是由于他并不是重新调度,所以还是同一个cpu,即占用资源,但是随着时间发展,可能也会在某个时候改变cpu核心,只是可能性很少),但在对应的地方直接等待了(下次到对应等待的yield后面执行)
static void sleep(times),使当前线程从 Running(Runnable,这是一个状态,要注意哦) 放弃处理器进入Block状态,休眠times毫秒
再返回到Runnable如果其他线程打断当前线程的Block(sleep)(还是使用当前线程的引用,比如可以考虑静态的), 就会发生InterruptedException
上面对状态的说明,了解即可,具体可以查看101章博客
因为有阻塞状态,即休眠的毫秒数,所以(间接,实际上是没有的)占用CPU资源(解释:指的是监视,而非sleep自身占用(对与自身,在阻塞时自然没有占用cpu资源,也就会释放对应资源,使得cpu资源回收,大多数博客说明的都是这个),一般监视是其他某些线程考虑的,所以这也是为什么大多数博客说他没有占用的原因,但也正是因为这样才会一直占用,而wait什么的是等待唤醒,没有什么一直操作的执行,因为是手动的,所以他完全不占用),因为休眠不结束的话,就会一直运行
但会有他里面代码的内存占用,但占用内存是必然的,其他程序也都需要内存,所以这里不用考虑内存
int getPriority(),获取线程的优先级
void setPriority(int newPriority),修改线程的优先级
优先级越高的线程不一定先执行,但该线程获取到时间片的机会会更多一些(一般总体时间更长)
除了程序也有运行时间,过大,则会后出结果
最主要的就是设置的优先级,不会真正的优先,就如概率一样,99%正确也会有1%的错误,可以将优先级看成概率
优先级默认为5 ,当然,优先级是看相对的,如果你的优先级是99,他也是99,那么就与5和5是一样的,所以是相对的,即我占二分之1,即50%概率,一般来说他们概率都相同(5),所以抢占基本随机
void join(),等待该线程终止,即主线程会等待,但是主线程里的其他子线程有启动的,那么那个子线程会继续运行
而不会等待
void join(long millis),等待参数指定的毫秒数
boolean isDaemon(),用于判断是否为守护线程
void setDaemon(boolean on),用于设置线程为守护线程
其他程序运行时间,也会导致sleep的操作
如我要一个数值输出8次,每停顿一秒输出一次,而主线程停顿8秒,打印主线程等待结束,通常情况下,都会输出8次
但是我在第一次的时候,就写一个很大的循环,可以循环8秒的循环,那么就会出现,只输出一次的结果
即可以对于一般程序来说,基本不会影响,因为他所占的时间对于一秒来说可以忽略不计
如主线程停5秒,子线程停1秒,主线程的5秒会导致子线程的判断条件为false,如下
white(a == true){
System.out.println(1);
Thread.sleep(1000);
}
Thread.sleep(5000);
a = false;
1-------- 1-------- 1-------- 1-------- 1-------- a == ture(不成立)
-------- -------- -------- -------- -------- a =false
white(a == true){
Thread.sleep(1000);
System.out.println(1);
}
Thread.sleep(5000);
a = false;
--------1 --------1 --------1 --------1 --------1 a == ture(不成立)
-------- -------- -------- -------- -------- a =false
package com.lagou.task18;
public class ThreadPriorityTest extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("子线程中:i = " + i);
}
}
public static void main(String[] args) {
ThreadPriorityTest tpt = new ThreadPriorityTest();
tpt.setPriority(Thread.MAX_PRIORITY);
tpt.start();
Thread t1 = Thread.currentThread();
for (int i = 0; i < 20; i++) {
System.out.println("--主线程中:i = " + i);
}
}
}
package com.lagou.task18;
public class ThreadJoinTest extends Thread {
@Override
public void run() {
System.out.println("倒计时开始...");
for (int i = 10; i > 0; i--) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("新年快乐!");
}
public static void main(String[] args) {
ThreadJoinTest tjt = new ThreadJoinTest();
tjt.start();
System.out.println("主线程开始等待...");
try {
tjt.join(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("可惜不是你,陪我到最后!");
}
}
package com.lagou.task18;
public class ThreadDaemonTest extends Thread {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("子线程中:i = " + i);
}
}
public static void main(String[] args) {
ThreadDaemonTest tdt = new ThreadDaemonTest();
tdt.setDaemon(true);
tdt.start();
for (int i = 0; i < 20; i++) {
System.out.println("-------主线程中:i = " + i);
}
}
}
主线程:是产生其他子线程的线程,通常它必须最后完成执行比如执行各种关闭动作
即可以说守护线程是给主线程操作子线程的一种设置
案例题目------------------------------
编程创建两个线程,线程一负责打印1 ~ 100之间的所有奇数,其中线程二负责打印1 ~ 100之间的所有偶数
在main方法启动上述两个线程同时执行,主线程等待两个线程终止
package com.lagou.task18;
public class SubThread1 extends Thread {
@Override
public void run() {
for (int i = 1; i <= 100; i += 2) {
System.out.println("子线程一中: i = " + i);
}
}
}
package com.lagou.task18;
public class SubThread2 extends Thread {
@Override
public void run() {
for (int i = 2; i <= 100; i += 2) {
System.out.println("------子线程二中: i = " + i);
}
}
}
package com.lagou.task18;
public class SubThreadTest {
public static void main(String[] args) {
SubThread1 st1 = new SubThread1();
SubThread2 st2 = new SubThread2();
st1.start();
st2.start();
System.out.println("主线程开始等待...");
try {
st1.join();
st2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程等待结束!");
}
}
package com.lagou.task18;
public class SubRunnable1 implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 100; i += 2) {
System.out.println("子线程一中: i = " + i);
}
}
}
package com.lagou.task18;
public class SubRunnable2 implements Runnable {
@Override
public void run() {
for (int i = 2; i <= 100; i += 2) {
System.out.println("------子线程二中: i = " + i);
}
}
}
package com.lagou.task18;
public class SubRunnableTest {
public static void main(String[] args) {
SubRunnable1 sr1 = new SubRunnable1();
SubRunnable2 sr2 = new SubRunnable2();
Thread t1 = new Thread(sr1);
Thread t2 = new Thread(sr2);
t1.start();
t2.start();
System.out.println("主线程开始等待...");
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程等待结束!");
}
}
线程同步机制------------------------------
当多个线程同时访问同一种共享资源时,可能会造成数据的覆盖等不一致性问题
即当多个线程需要访问同一个资源时
它们需要以某种顺序来确保该资源在某时刻只能被一个线程使用,否则,程序的运行结果将会是不可预料的
此时就需要对线程之间进行通信和协调,该机制就叫做线程的同步机制
多个线程并发读写同一个临界资源时会发生线程并发安全问题
异步操作:多线程并发(一起进发)的操作,各自独立运行,可以理解位不同的步骤,即不用排队
同步操作:多线程串行(跟着进行)的操作,先后执行的顺序,可以理解为同样的步骤,即排队
对于同步机制,主要针对于多核,因为单核实际上还是依次运行的
对于多个start方法,先调用的一般先执行,只不过现在的电脑处理很快的,你几乎都感觉不到在并发
但也只是先执行的速度快而已,CPU先运算的还是会先执行
解决方案------------------------------
先看后面的代码:
由程序结果可知:当两个线程同时对同一个账户进行取款时,导致最终的账户余额不合理
引发原因:线程一执行取款时还没来得及将取款后的余额写入后台,线程二就已经开始取款
解决方案:让线程一执行完毕取款操作后,再让线程二执行即可,将线程的并发操作改为串行操作
经验分享:在以后的开发尽量减少串行操作的范围,从而提高效率
实现方式------------------------------
在Java语言中使用synchronized关键字来实现同步/对象锁机制从而保证线程执行的原子性
即要么我不执行,要么等我执行完,且执行过程不可打断,若有多个在等待,那么他们自己之间进行抢夺资源
来保证谁先进入,即也相当于操作系统调度算法来决定
具体方式如下:
使用同步代码块的方式实现部分代码的锁定,格式如下:
synchronized(类类型的引用) {
编写所有需要锁定的代码
}
使用同步方法的方式实现所有代码的锁定,直接使用synchronized关键字来修饰整个方法即可该方式等价于:
synchronized(this) {
整个方法体的代码
}
即直接用synchronized对方法修饰时,就说明这个方法是谁调用谁来当同步监听器,即等价于this
换言之,就是这个方法被锁了,只对于同一引用,因为其他引用调用的this是不同的
即这就是为什么需要一个同步监听器的原因
由于synchronized(类类型的引用)的"类类型的引用"是多变的,即不同的引用也会参与同步,如静态的引用
但基本上都会在类里面新建该引用,防止同一引用的调用,会出现不同步,如synchronized(new 类名())
但是正是因为有可能会没有新建,则出现了直接修饰整个方法,即相当于this,这样就保证了是同一引用的会同步(认为是对该引用加上锁的相关标识,在以后或说明的,比如101章博客中的state状态的线程标识位)
而对于静态来说,由于没有this,但是静态的基本上都是一个类的,即使用了该静态,就一定是使用了同一个类的方法
那么这时使用synchronized修饰方法时等价于使用了synchronized(类名.class)(通常是当前类,即静态方法所在的类),这个类名无论怎么变化,他们在调用这个静态方法时都会同步(串行)
最后可以理解为无论是什么方法或者代码,在有synchronized锁面前,只要()里面的同步监听器是相同的
相同监听器所修饰的锁都必须等待前一个的锁打开(运行完,而Lock必须解锁)
否则,无论你是不同的方法,还是不同的类的方法,都必须等一等
回忆:继承类时相同方法都有重写,一个类的方法是在方法区里,且独一份,并且类里面最好不要创建本类的对象
因为有嵌套,如main方法里创建对象时,运行时有问题,或者静态对象调用时,也会出现问题
当然静态变量在本类方法里,可以直接写出,或者加上类名写出都可以
package com.lagou.task18;
public class AccountThreadTest extends Thread {
private int balance;
private static Demo dm = new Demo();
public AccountThreadTest() {
}
public AccountThreadTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public void run() {
test();
}
public static void test() {
synchronized (AccountThreadTest.class) {
System.out.println("线程" + Thread.currentThread().getName() + "已启动...");
int temp = 1000;
if (temp >= 200) {
System.out.println("正在出钞,请稍后...");
temp -= 200;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走您的钞票!");
} else {
System.out.println("余额不足,请核对您的账户余额!");
}
}
}
public static void main(String[] args) {
AccountThreadTest att1 = new AccountThreadTest(1000);
att1.start();
AccountThreadTest att2 = new AccountThreadTest(1000);
att2.start();
System.out.println("主线程开始等待...");
try {
att1.join();
att2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终的账户余额为:" + att1.getBalance());
}
}
package com.lagou.task18;
import java.util.concurrent.locks.ReentrantLock;
public class AccountRunnableTest implements Runnable {
private int balance;
private Demo dm = new Demo();
private ReentrantLock lock = new ReentrantLock();
public AccountRunnableTest() {
}
public AccountRunnableTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public void run() {
lock.lock();
System.out.println("线程" + Thread.currentThread().getName() + "已启动...");
int temp = getBalance();
if (temp >= 200) {
System.out.println("正在出钞,请稍后...");
temp -= 200;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走您的钞票!");
} else {
System.out.println("余额不足,请核对您的账户余额!");
}
setBalance(temp);
lock.unlock();
}
public static void main(String[] args) {
AccountRunnableTest account = new AccountRunnableTest(1000);
Thread t1 = new Thread(account);
Thread t2 = new Thread(account);
t1.start();
t2.start();
System.out.println("主线程开始等待...");
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终的账户余额为:" + account.getBalance());
}
}
class Demo{}
注意:一个类里面可以写多个类(不是内部类),而文件名通常必须要与有public修饰的类名一样,否则编译时会报错
之所以是通常,是因为若都没有public那么就会编译通过,会出现多个类的字节码文件
即使用java xxx(那些字节码文件的名字,不修改的话即类名),会使用相应的类的main方法
但是若直接用java xxx.java则会执行,他跳过了该错误
而他没有直接的看到出现的字节码文件,那么在运行时,会根据类的执行顺序而运行main方法
且不管你是否为public修饰的类,即只运行放在前面的类的main方法
静态方法的锁定------------------------------
当我们对一个静态方法加锁,如:
public synchronized static void xxx(){….}
那么该方法锁的对象是类对象
每个类都有唯一的一个类对象
获取类对象的方式:类名.class
静态方法与非静态方法同时使用了synchronized后它们之间是非互斥关系的
原因在于:静态方法锁的是类对象而非静态方法锁的是当前方法所属对象
静态变量是有引用的连接的,即也可以用引用来调用
所有的修饰都基本上写在返回值前面,而返回值前面的修饰基本上是可以改变位置的
注意事项------------------------------
使用synchronized保证线程同步应当注意:多个需要同步的线程在访问同步块时,看到的应该是同一个锁对象引用
即当使用类实现接口时,由于Thread都使用该类,即调用该类的run方法,最后创建多个Thread时
调用的都是同一个对象的run方法
即synchronized里的引用若使用本类创建的引用时,使用的是同一个
若使用类继承Thread的话,那么调用的是不同的对象的run方法,那么synchronized里的引用若使用本类创建的引用时
使用的不是同一个
在使用同步块时应当尽量减少同步范围以提高并发的执行效率
线程安全类和不安全类------------------------------
StringBuffer类是线程安全的类,但StringBuilder类不是线程安全的类
Vector类和 Hashtable类是线程安全的类,但ArrayList类和HashMap类不是线程安全的类,一般我们都会考虑使用不安全的,因为效率高
Collections.synchronizedList() 和 Collections.synchronizedMap()等的返回结果对象中,方法基本都是安全的
死锁的概念------------------------------
线程一执行的代码:
public void run(){
synchronized(a){
synchronized(b){
编写锁定的代码;
}
}
}
线程二执行的代码:
public void run(){
synchronized(b){
synchronized(a){
编写锁定的代码;
}
}
}
注意:在以后的开发中尽量减少同步的资源,减少同步代码块的嵌套结构的使用
使用Lock(锁)实现线程同步------------------------------
从Java5开始提供了更强大的线程同步机制—使用显式定义的同步锁对象来实现
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具
该接口的主要实现类是ReentrantLock类,该类拥有与synchronized相同的并发性
在以后的线程安全控制中,经常使用ReentrantLock类显式加锁和释放锁
常用的方法------------------------------
ReentrantLock(),使用无参方式构造对象
void lock(),获取锁
void unlock(),释放锁
具体代码在前面操作过了
与synchronized方式的比较------------------------------
Lock是显式锁,需要手动实现开启和关闭操作
而synchronized是隐式锁,执行锁定代码后自动释放
Lock只有同步代码块方式的锁,即锁与解锁之间的代码
与synchronized不一样,synchronized是运行完后,才可让其他代码进入,而Lock必须要解锁后才可进入
而synchronized有同步代码块方式和同步方法两种锁
使用Lock锁方式时,Java虚拟机将花费较少的时间来调度线程,因此性能更好
由于Lock可以实现手动操作,即可以有更多的操作空间
Object类常用的方法------------------------------
void wait(),用于使得线程进入等待状态(也是阻塞状态,类似于sleep方法,他们都是阻塞状态,只是阻塞的方式不同而已),直到其它线程调用notify()或notifyAll()方法
且占用CPU资源(针对判断他的位置来说的,否则是没有占用的,就如sleep的监视一样,并且他的唤醒是重新调度,所以可能回到其他cpu核心,在同一个cpu核心上,就代表执行权没有改变,一般的,这都是因为算法来判断的,而重新调度,自然是多核心情况下,所以他都是有可能的,而sleep一般不会这样),并会解除对象锁,而sleep不会
wait()方法会强迫线程先进行释放锁操作,即必须要在锁里面进行,否则报错
即可以知道,他可以随时释放锁,与unlock方法类似(只是类似,因为他释放的是synchronized锁)
但他也可以让该线程进行等待(对应地方等待,只是其他的线程可以进入,但他还是在哪个地方,并没有离开,即没有结束执行),且必须唤醒或者等待他时间结束(后面的操作方法),这里是必要的
所以也可以看成sleep与unlock的结合体,即可以有更多的操作空间
如互相输出,即线程的来回操作
也可以让两个线程同时操作,如当唤醒后或者等待时间结束后,一个操作wait方法前面的,另一个操作wait方法后面的
但必须要在synchronized锁里面,Lock锁里面可能也不行(实际上也可以,具体可以百度,一般不能直接的操作,需要在里面又加上synchronized,特别的,若要直接实现对应的操作,一般需要Condition,一般是接口,通常使用ReentrantLock来进行创建对象),因为底层代码的缘故,wait()方法会强迫线程先进行释放锁操作
即必须要在synchronized锁里面进行,否则报错,因为wait就是用来操作锁的,即在锁里面调用wait方法时
可以隐式的看成监听器.wait(),所以当syschronized的参数为this时,可以直接写wait(),否则需要加参数名.wait()(当然,如果对应的参数是直接new的,那么基本上不能显示调用了),即如果有参数,那么必须参数来调用(比如Object a = new Object();,syschronized参数设置为a,那么就要a.wait(),否则直接的wait()会报错),而new基本不能调用,否则也报错
除非他有隐式的this,如引用调用时,就有this,要不然就报错
即虽然是服务与锁,但实际上服务于监听器,即锁可以称为监听器的锁
那么就会有,当调用方法时,若引用相同(设为引用为c),且该方法里的synchronized的监听器为this
那么该锁里面写上wait或者notify,也就是c.wait或者c.notify,后者会将拥有同样监听器的锁唤醒,而前者不会
因为wait方法作用于本身线程,与其他线程无关,而notify方法可以让其他线程调用,来唤醒同样监听器的线程
其中notify是c.notify可以让该类型锁里面的wait唤醒,而wait虽然也是c.wait
但实际上他并没有让其他该类型锁的线程等待,前者可以通过找到wait方法,而进行唤醒
后者却找不到让其他线程等待的东西,即只可以使得当前线程等待
这里"监听器"实际叫做"同步监听器"
当你认为输出结果与预期不对时,有可能是输出的那个语句没看到了,因为太慢了,即会很少
void wait(long timeout),用于进入等待状态,直到其它线程调用方法或参数指定的毫秒数已经过去为止
即当等待时间到了时,就会自动解除等待,当然,也可以在他等待时直接唤醒
void notify(),用于唤醒等待的单个线程(如果有多个线程,那么随机选中一个来唤醒,并没有什么顺序),与wait方法一样,有this可以不加,有参数,加参数
void notifyAll(),用于唤醒等待的所有线程,与wait方法一样,有this可以不加,有参数,加参数
解决wait方法和sleep方法是否会释放锁的问题
public synchronized void run(){
notify();
System.out.println(1);
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(2);
}
package com.lagou.task18;
public class ConsumerThread extends Thread {
private StoreHouse storeHouse;
public ConsumerThread(StoreHouse storeHouse) {
this.storeHouse = storeHouse;
}
public ConsumerThread() {
}
@Override
public void run() {
while (true) {
storeHouse.consumerProduct();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.lagou.task18;
public class ProduceThread extends Thread {
private StoreHouse storeHouse;
public ProduceThread(StoreHouse storeHouse) {
this.storeHouse = storeHouse;
}
@Override
public void run() {
while (true) {
storeHouse.produceProduct();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.lagou.task18;
public class StoreHouse {
private int cnt = 0;
public synchronized void produceProduct() {
notify();
if (cnt < 10) {
System.out.println("线程" + Thread.currentThread().getName() +
"正在生产第" + (cnt+1) + "个产品...");
cnt++;
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void consumerProduct() {
notify();
if (cnt > 0) {
System.out.println("线程" + Thread.currentThread().getName() + "消费第" + cnt + "个产品");
cnt--;
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.lagou.task18;
public class StoreHouseTest {
public static void main(String[] args) {
StoreHouse storeHouse = new StoreHouse();
ProduceThread t1 = new ProduceThread(storeHouse);
ConsumerThread t2 = new ConsumerThread(storeHouse);
t1.start();
t2.start();
}
}
线程池------------------------------
从Java5开始新增加创建线程的第三种方式为实现java.util.concurrent.Callable接口
常用的方法------------------------------
V call(),计算结果并返回
FutureTask类------------------------------
public class FutureTask<V>
extends Object
implements RunnableFuture<V>
java.util.concurrent.FutureTask类用于描述可取消的异步计算
该类提供了Future接口的基本实现,包括启动和取消计算
查询计算是否完成以及检索计算结果的方法,也可以用于获取方法调用后的返回结果
常用的方法------------------------------
FutureTask(Callable callable),根据参数指定的引用来创建一个未来任务
V get(),获取call方法计算的结果,并等待线程执行完毕
package com.lagou.task18;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadCallableTest implements Callable {
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 10000; i++) {
sum +=i;
}
System.out.println("计算的累加和是:" + sum);
return sum;
}
public static void main(String[] args) {
ThreadCallableTest tct = new ThreadCallableTest();
FutureTask ft = new FutureTask(tct);
Thread t1 = new Thread(ft);
t1.start();
Object obj = null;
try {
obj = ft.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("线程处理方法的返回值是:" + obj);
}
}
线程池的由来------------------------------
在服务器编程模型的原理,每一个客户端连接用一个单独的线程为之服务,当与客户端的会话结束时,线程也就结束了
即每来一个客户端连接,服务器端就要创建一个新线程
如果访问服务器的客户端很多,那么服务器要不断地创建和销毁线程,这将严重影响服务器的性能
概念和原理------------------------------
线程池的概念:首先创建一些线程,它们的集合称为线程池,当服务器接受到一个客户请求后
就从线程池中取出一个空闲的线程为之服务,服务完后不关闭该线程,而是将该线程还回到线程池中
在线程池的编程模式下,任务是提交给整个线程池,而不是直接交给某个线程
线程池在拿到任务后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程
任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务
相关类和方法------------------------------
public class Executors
extends Object
从Java5开始提供了线程池的相关类和接口:java.util.concurrent.Executors类和java.util.concurrent.ExecutorService接口
其中Executors是个工具类和线程池的工厂类,可以创建并返回不同类型的线程池
常用的方法------------------------------
static ExecutorService newCachedThreadPool(),创建一个可根据需要创建新线程的线程池
static ExecutorService newFixedThreadPool(int nThreads),创建一个可重用固定线程数的线程池
static ExecutorService newSingleThreadExecutor(),创建一个只有一个线程的线程池
其中ExecutorService接口是真正的线程池接口,主要实现类是ThreadPoolExecutor
常用的方法------------------------------
void execute(Runnable command),执行任务和命令,通常用于执行
Runnable Future<V> submit(Callable<T> task),执行任务和命令,通常用于执行
Callablevoid shutdown(),启动有序关闭
package com.lagou.task18;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.submit(new ThreadCallableTest());
executorService.shutdown();
}
}
对于io流来说,在多线程的情况下,并不共享文件的下标,每个创建的流是自带自己的下标的(注意是创建的流,如果是同一个流,比如赋值给你,那么自然会共享其状态,所以在某种程度上是一个流一个状态,而大多数是多线程状态,所以也可以说成是多线程情况下,但最终的原因还是是因为不同的流,所以在某些情况下,即多线程的情况并不绝对的),唯一共享的是文件,所以在并发情况下,对于文件的写入,可能存在被覆盖的操作,这里注意即可