一、相关概念
1.1首先了解什么是程序、线程、进程
程序:指为完成特定任务,用某种语言编写的一组指令的集合,通俗易懂来讲程序就是一段静态的代码;
线程:程序组成线程,线是程序内部的一条执行路径,对于程序而言线程的动态的,是CPU调度和执行的最小单位;
进程:线程组成进程,是程序的一次执行的过程,或是在在内存中运行的运用程序,如:打开的QQ/WX,进程至少由一个线程组成、是操作系统调度和分配资源的最小单位,一个进程若执行多个线程,就是支持多线程的。
1.2多线程优缺点
一个进程中多个线程共享相间的内存单元,他们从同一个堆中分配对象,可以访问相间的对象,提升了计算机系统CPU的利用率使得线程间的通信更加简洁、高效,但多个线程共享资源会带来安全隐患。·
1.3线程调度
分时调度:所有线程轮流使用CPU的使用权,并且平均分配每个线程占用CPU的时间。
抢占式调度:让优先级高的线程更大概率能够优先使用到CPU(java使用)
1.4并行与并发
并行:指两个或多个事件在同一时刻发生,指多个指令在多个CPU中同时执行,如:多个人同时在做不同的事情
并发:指两个或多个事件在同一时间段内发生,即一个时间段内有多条指令在单个CPU上快速切换,使得在宏观上具有多个进程在同时执行的效果
二、多线程的创建方式(四种)
2.1继承Thread类
(其实通过源码可以看到,继承的该Thread类也是实现了Runnable接口)
①定义Thread类的子类,并重写run()方法(方法体中代表了线程需要完成的任务)
②创建子类的实例(即创建了线程对象)
③通过子类调用线程对象的star()方法启动线程(启动线程后start方法会调用run方法,如果直接调用run方法线程未启动)
例子:创建分线程1遍历100内的偶数,线程2遍历100内的奇数
package Mon07.day0722;
public class ThreadTest{
public static void main(String[]args){
Thread1 t1 = new Thread1();
Thread2 t2 = new Thread2();
t1.start();
t2.start();
//注意:若还想建立新的R2线程,可以直接实例化并启动新线程
Thread t3 = new Thread();
t3.start();
}
}
class Thread1 extends Thread{
@Override
public void run(){
for(int i = 0 ; i <= 100 ; i++){
if(i%2 == 0){
Thread.currentThread().setName("线程1");
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
class Thread2 extends Thread {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 != 0) {
Thread.currentThread().setName("线程2");
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
2.2实现Runnable接口
①定义实现Runnable接口的子类,并重写run()方法(方法体中代表了线程需要完成的任务)
②创建子类的实例(即创建了线程对象)
③通过子类传入Thread类后再调用线程对象的star()方法启动线程(此子类和Thread类都实现了Runable接口,但是还是传入Thread类,是因为采用了代理)
案例
package Mon07.day0722;
public class RunnableTest {
public static void main(String[] args) {
Runnable1 R1 = new Runnable1();
Thread t1 = new Thread(R1);
t1.start();
Runnable2 R2 = new Runnable2();
Thread t2 = new Thread(R2);
t2.start();
//注意:若还想建立新的R2线程,可以直接实例化并启动新线程
Thread t2 = new Thread(R2);
t2.start();
}
}
class Runnable1 implements Runnable{
@Override
public void run() {
for(int i = 0 ; i <= 100 ; i++){
if(i%2 == 0){
Thread.currentThread().setName("线程1");//线程命名
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}class Runnable2 implements Runnable{
@Override
public void run() {
for(int i = 0 ; i <= 100 ; i++){
if(i%2 != 0){
Thread.currentThread().setName("线程2");//线程命名
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
拓展:继承Thread类和继承Runnable接口的区别
共同点:
①都要调用Thread类中定义的start()方法
②创建的线程对象都是Thread类或其子类的实例
不同点:一个是类的继承,一个是接口的实现
建议:常用Runnable接口实现
使用Runnable接口的好处在于:①该实现方法可以避免继承的局限性②更适合处理有共享数据的问题(继承需要通过静态共享数据,但接口数据天然 就会共享)
2.4实现Callable接口
(jdk5新增)
与之前的方法runnable()进行对比,有哪些好处?
1.call()可以有返回值,却因为callable使用了泛型参数,所以call()的返回值类型会更加灵活;
2.call()可以使用throws方式处理异常,更加灵活。
package Mon09.day0903;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class NumThread implements Callable{
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
if (i%2 ==0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class CallableTest {
public static void main(String[] args) {
NumThread numThread = new NumThread();//创建callable接口实现类对象
FutureTask futureTask = new FutureTask(numThread);//将对象传递到FutureTask构造器中,创建futureTask对象
Thread thread = new Thread(futureTask);//将futureTask对象传递到Thread中,创建Thread对象并调用start()
thread.start();
try {
Object sum = futureTask.get();
//主线程带出,但如果此时主线程下、需要获取分线程call()的返回值,则此时主线程自带阻塞
System.out.println("总和为:"+sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
2.5 使用线程池
好处:
1.提高了程序执行的效率(因为线程已经提前创建好了)
2.提高了资源的复用率(因为执行完的线程并未销毁,而是可以继续执行其它的任务)
3.可以设置相关的参数,对线程池中的线程进行管理。
三、Thread的常用方法
3.1多线程的常用结构
3.1.1线程中的构造器
public Thread():分配一个新的线程对象;
public Thread(String name):分配一个指定名称的新线程对象;
public Thread(Runnale target):指定创建线程的目标对象,它实现了Runnable接口的run方法;
public Thread(Runnale target,String name):分配一个带有指定目标且指定名称的新线程对象;
3.1.2线程的常用方法
start():①启动线程 ②调用线程的run()
run():将线程要执行的操作,声明在run()中。
currentThread():获取当前执行代码对应的线程
getName():获取线程名
setName():设置线程名
sleep(long millis):静态方法,调用时,可以使得当前线程睡眠指定的毫秒数
yield():静态方法,一旦执行即可能主动释放本次CPU的执行器
join():在线程a中通过线程b调用join(),以为着线程a进入阻塞状态,直到线程b执行结束,线程a才会结束阻塞状态继续执行
isAlive():判断当前线程的存活情况
getPriority():获取线程的优先级,未设置的默认为5级(1-10级)
三个优先级常量:MAX_PRIORITY(10),MIN_PRIORITY(1),NORM_PRIORITY(5)
setPriority(int newPriority):改变线程的优先级,范围[1,10]
过时方法(了解即可):
public final void stop(): 强行结束一个线程的执行,直接进入死亡状态。run()即刻停止,可能会导致一些清理性的工作得不到完成,如文件,数据库等的关闭。同时,会立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题。
void suspend()/void resume():这两个操作就好比播放器的暂停和恢复。二者必须成对出现,否则非常容易发生死锁。suspend()调用会导致线程暂停,但不会释放任何锁资源,导致其它线程都无法访问被它占用的锁,直到调用resume()。已过时 ,不建议使用。
四、多线程的生命周期
了解即可。
五、如何使用同步机制解决线程安全问题(synchronized)
关于什么是线程安全问题:当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题。但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。
5.1同步代码块
synchronize(同步监视器){
//需要被同步的代码
}
说明:
1.需要被同步的代码,即为操作共享数据的代码。
2.共享数据:即多个线程多需要操作的数据。比如:ticket
3.需要被同步的代码,在被synchronized包裹以后,就使得一个线程在操作这些代码的过程中,其它线程必须等待,同步监视器,俗称锁。哪个线程获取了锁,哪个线程就能执行需要被同步的代码。
4.同步监视器,可以使用任何一个类的对象充当。但是,多个线程必须共用同一个同步监视器。
重点:在实现Runnable接口的方式中,同步监听器/锁 可以考虑使用this
在继承Thread类的方式中,同步监听器慎用this(很可能多个实现类this不唯一)
package day0801;
import sun.plugin2.os.windows.Windows;
class wimdow extends Thread{
static int ticket = 100;
static Object obj = new Object();
public void run(){
while(true){
//synchronized(this){//实例化多个对象是,this代表t1 t2 t3,不能代表唯一性
// synchronized(obj){//obj若静态,则可以保证唯一性
synchronized(Windows.class){
if(ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"售票,票号:"+ticket);
ticket--;
}else{break;}
}
}
}
}
public class Window{
public static void main(String[] args) {
wimdow w = new wimdow();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.start();
t2.start();
t3.start();
}
}
5.2同步方法
如果操作共享数据的代码完整声明在了一个方法中,那么我们就可以将此方法声明为同步方法即可。
非静态的同步方法,默认同步监听器为this;
静态的同步方法,默认同步监听器为当前类本身。
package day0803;
import sun.plugin2.os.windows.Windows;
class wimdow1 extends Thread{
static int ticket = 100;
static Object obj = new Object();
boolean isFlag = true;
public synchronized void run(){
while(isFlag){
show();
}
}
public void show(){
if(ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"售票,票号:"+ticket);
ticket--;
}else {isFlag = false;}
}
}
public class WindowTest1 {
public static void main(String[] args) {
wimdow1 w = new wimdow1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.start();
t2.start();
t3.start();
}
}
5.3synchronize好处与弊端
好处:解决了线程的安全问题
弊端:在操作共享数据时,多线程是串行执行的,意味着性能变低。
六、死锁
是什么
不同线程分别占用对方需要的同步资源不放弃,都在等对方放弃自己需要的同步资源,导致形成死锁。
死锁出现后,程序不会发生异常,不会给出提示,只是所有线程处于阻塞状态无法继续。
诱发死锁的原因
-条件互斥
-占用且等待
-不可抢夺
循环等待
以上四个条件,同时出现则会触发死锁。
如何避免死锁
针对条件1:互斥条件基本无法被破坏,因为线程需要通过互斥解决安全问题
针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待问题
针对条件3:占用部分资源的线程进一步申请其它资源时,如果申请不到则主动释放已经占用的资源
针对条件4:可以将资源改为线性顺序。申请资源时先申请序号较小的,避免循环等待问题。
七、Lock锁的使用
使用步骤:
1.创建lock的实例,确保多个线程公用一个lock实例(static final)
2.执行lock方法,锁定共享数据的方法
3.unlock()的调用,释放对共享数据的锁定
案例:
package day0828;
import java.util.concurrent.locks.ReentrantLock;
class Window extends Thread{
static int ticket = 100;
//1.创建lock的实例,确保多个线程公用一个lock实例(static final)
private static final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
//2.执行lock方法,锁定共享数据的方法
lock.lock();
if (ticket > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"售票,售票号为:"+ticket);
ticket--;
}else {break;}
}finally {
//3.unlock()的调用,释放对共享数据的锁定
lock.unlock();
}
}
}
}
package day0828;
public class LockTest {
public static void main(String[] args) {
Window window = new Window();
window.start();
}
}
面试题:
一、synchronized方法与lock的对比?
synchronized不管是同步代码块还是同步方法,都需要在结束一对{}后释放对同步监视器的调用。
lock是通过两个方法控制需要被同步的代码,更加灵活一些。
Lock作为接口,提供了多种实现类,适合更多复杂的场景,效率更高
二、wait和sleep方法的区别?
相同点:一旦执行,当前线程就会进入阻塞状态
不同点:
声明的位置:wait声明在Object中,sleep在Thread类中且为静态
使用的场景不同:wait只能在同步代码块或同步方法中,sleep在任何需要使用的场景中
使用在同步代码块中时:wait一旦执行则会释放同步监视器,sleep不会释放。
结束阻塞的方式:wait:到达指定时间自动结束或者notify、notifyAll唤醒,sleep通过指定时间自动结束
八、线程通信
当我们需要多个线程有规律的共同执行一个任务时,多线程间就需要一些通信机制来协调它们的工作。
比如:线程A用来生产包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,此时B线程必须等到A线程完成后才能执行,那么线程A与线程B之间就需要线程通信,即——等待唤醒机制。
等待唤醒机制关键字:
wait:线程不再活动、参与调度,进入到wait set中,因此不会浪费CPU资源,不会竞争锁,此时线程的状态会转变为WAITING或者TMED_WAITING。只有当别的线程执行了一个特别动作:通知(notify)或者等待时间到,这个对象上等待的现场才会从wait set中释放出来,重新进入到调度队列(ready queue)中。
notify:选取所通知对象的wait set中一个线程释放;
notifyAll:释放所通知对象的wait set中所有线程;
注意:
1.此三个方法必须是在同步代码块或同步方法中。(需要配合condition实现线程间的通信)
2.此三个方法的调用者必须是同步监视器【synchronized (this)中的this】
例子:
package day0828;
/*
使用两个线程交替打印1-100
*/
import javafx.scene.control.TableRow;
class PrintNumber implements Runnable{
private int number = 1;
@Override
public void run() {
synchronized (this) {
while (true){
notify();
if (number <= 100){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+number);
number++;
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {break;}
}
}
}
}
public class PrintNumberTest {
public static void main(String[] args) {
PrintNumber p = new PrintNumber();
Thread t1 = new Thread(p, "线程1");
Thread t2 = new Thread(p, "线程2");
t1.start();
t2.start();
}
}
九、案例
生产者Productor将产品交给店员Clerk,消费者从店员取走,店员最多持有20件产品,超过20件的话就会通知生产者停止生产,少于20件后才会恢复生产,若无产品店员则会让消费者稍等,等有产品后再通知消费者消费。
分析:多线程为消费者、生产者,共享数据为店员的产品(存在共享数据就会有线程安全问题)。
package Mon08.day0830;
/**
*
* 生产者Productor将产品交给店员Clerk,消费者从店员取走,店员最多持有20件产品,超过20件的话
* 就会通知生产者停止生产,少于20件后才会恢复生产,
* 若无产品店员则会让消费者稍等,等有产品后再通知消费者消费。
*/
//店员
class Clerk{
private int productNum = 0;//产品初始数量
public synchronized void addProductNum() {
if (productNum >= 20){
try {
wait();//等待
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName()+"生产了第"+productNum+"个产品");
productNum++;
notify();
}
public synchronized void minusProductNum() {
if (productNum <= 0){
try {
wait();//等待
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName()+"消费了第"+productNum+"个产品");
productNum--;
notify();
}
}
//生产者
class Productor extends Thread{
private Clerk clerk;
public Productor(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
while (true){
System.out.println("生产者生成产品……");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
clerk.addProductNum();
}
}
}
//消费者
class Comsumer implements Runnable{
private Clerk clerk;
public Comsumer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
while (true){
System.out.println("消费者开始消费产品");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
clerk.minusProductNum();
}
}
}
public class ProducerComsumerTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor p1 = new Productor(clerk);
Comsumer c1 = new Comsumer(clerk);
p1.start();
Thread thread = new Thread(c1);
thread.start();
p1.setName("生产者1");
thread.setName("消费者1");
}
}
#在职重学JAVA基础
【文章原创作者:防ddos攻击 http://www.558idc.com/shsgf.html 复制请保留原URL】