告别 2019, 迎接 2020,元旦快乐。 2019 真是不平凡的一年,困难伤病,接踵而至,唏嘘呼哉,过眼云烟。
2020,鼎力前行,多多阅读。
现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。
多线程一般用于的场景:
1. GUI 应用程序
2. 耗时的I/O 处理
3. 多个客户端
1.创建线程的方法
1.1. 通过继承Thread 类,实现run方法
由于Java是单继承的,所以继承于Thread 类的使用会有一定的局限性
class MyThread extends Thread{
public void run() { // 实现 run 方法
System.out.println("线程--" + Thread.activeCount());
}
}
public class SemaphoreMain {
/**
* @param args
*/
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println("主线程的操作" + i);
}
for (int i = 0; i< 10; i++) {
new MyThread().start();
}
}
}
1.2. 通过实现Runnable 接口
class RunThread implements Runnable{
public void run() {
for (int i =0 ; i< 5; i++) {
System.out.println("实现Runnable 接口线程: "+ Thread.activeCount());
}
}
}
public class SemaphoreMain {
/**
* @param args
*/
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println("主线程的操作" + i);
}
new Thread(new RunThread()).start();
}
}
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
1.3 实现callable() 接口
callable() 接口一般是和 Future 接口一起使用的
Callable接口代表一段可以调用并返回结果的代码;Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。
在线程池提交Callable任务后返回了一个Future对象,使用它可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法让我们可以等待Callable结束并获取它的执行结果。
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
class testCallable implements Callable<Integer>{
private String word;
public testCallable(String word){
this.word = word;
}
@Override
public Integer call() throws Exception { // call() 方法可以抛异常
System.out.println(Thread.currentThread().getName() + ": 开始执行!" );
try {
//假设处理需要2秒
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 正在处理!" );
System.out.println(Thread.currentThread().getName() + ": " + word + "长度为:" + word.length());
return Integer.valueOf(word.length());
}
}
public class Test1{
public static void main(String [] args ) {
String [] words = {"one","two","three","four"};
//创建一个线程池
ExecutorService pool = Executors.newCachedThreadPool( );
Set<Future<Integer>> set = new HashSet<Future<Integer>>();
for (String word: words) {
Callable<Integer> callable = new testCallable(word);
Future<Integer> future = pool.submit(callable);
set.add(future);
}
int sum = 0;
for (Future<Integer> future : set) {
try {
sum += future.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
System.out.println("所有单词的总长度为:" + sum);
}
}
另外一个例子:
package com.example.newdemo.javatest;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class FutureTest {
// 实现Callable 接口
public static class Task implements Callable<String>{
@Override
public String call() throws Exception {
String threadName = Thread.currentThread().getName();
System.out.println("thread: " + threadName);
return threadName;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
List<Future<String>> res = new ArrayList<>();
// 创建一个线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++){
// 线程池的submit方法,返回结果为Future
Future<String> future = threadPool.submit(new Task());
res.add(future);
}
for (Future<String> re: res){
System.out.println(re.get()); // 使用get 方法获取返回值
}
}
}
Callable 接口和 Runnable接口的区别:
1、Callable重写的方法是call(),Runnable重写的方法是run().
2、Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
3、call方法可以抛出异常,run方法不可以
4、运行Callable任务可以拿到一个Future对象,表示异步计算的结果。
它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
2. 线程基础
1) 多线程的六个状态
Java线程变迁
线程创建之后,调用start()方法开始运行。当线程执行wait()方法之后,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态。线程在执行Runnable的run()方法之后将会进入到终止状态。
线程处于阻塞状态的三种形式:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
注意:
Java将操作系统中的运行和就绪两个状态合并称为运行状态。阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中Lock接口的线程状态却是等待状态,因为java.concurrent包中Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。
2)等待/通知范例:
该范式分为两部分,分别针对等待方(消费者)和通知方(生产者)。等待方遵循如下原则。
1)获取对象的锁。
2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。
对应的伪代码如下:
synchronized(对象) {
while(条件不满足) {
对象.wait();
}
对应的处理逻辑
}
通知方遵循如下原则:
1)获得对象的锁。
2)改变条件。
3)通知所有等待在对象上的线程。
对应的伪代码如下:
synchronized(对象) {
改变条件
对象.notifyAll();
}
3)终止线程操作
stop() 函数被废弃了
4)线程中断
和中断相关的3 个操作
public void Thread.interrupt(); //中断线程
public boolean Thread.isInterrupted(); // 判断是否中断
public static boolean Thread.interrupted(); // 判断是否中断,并清除当前中断状态
中断的使用:
5)join()等待线程方法,等待依赖的线程执行完毕,再执行
public final void join() throws InterruptedException
public final synchronized void join(long mills) throws InterruptedException
线程让步:Thread.yield() 方法
暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
使用yield 方法的实现
class MyThread extends Thread{
private String name;
public MyThread(String name){
this.name = name;
}
public void run(){
try{
for (int i = 0; i < 5; i++){
System.out.println(name + "--> " + i);
Thread.sleep(200);
if(i == 3){
this.yield();
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class Main {
public static void main(String[] args) {
MyThread m1 = new MyThread("A");
MyThread m2 = new MyThread("B");
m1.start();
m2.start();
}
}
6) 分门别类的管理:线程组
在一个系统中,如果线程数量多,而且功能分配比较明确,就可以将相同线程放在一起。
7) 守护线程
8)synchronized关键字的五种使用方法
1. 同步普通方法
只要涉及线程安全,上来就给方法来个同步锁。这种方法使用虽然最简单,但是只能作用在单例上面,如果不是单例,同步方法锁将失效。
/**
* 用在普通方法
*/
private synchronized void synchronizedMethod() {
System.out.println("synchronizedMethod");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
只有一个实例才能使用这种方法,如果是多个实例,则无法保证线程安全。
因此:同一个实例只有一个线程能获取锁进入这个方法。
如果想实现多个实例安全访问数据,则可以使用下列的同步静态方法
2.同步静态方法
不管有多少个类实例,同时只有一个线程能获取锁进入这个方法。
/**
* 用在静态方法
*/
private synchronized static void synchronizedStaticMethod() {
System.out.println("synchronizedStaticMethod");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
同步静态方法是类级别的锁,一旦任何一个线程进入这个方法,其他所有线程将无法访问这个类的任何同步类锁的方法。
3.同步类方法
下面提供了两种同步类的方法,锁住效果和同步静态方法一样,都是类级别的锁,同时只有一个线程能访问带有同步类锁的方法。
/**
* 用在类
*/
private void synchronizedClass() {
synchronized (TestSynchronized.class) {
System.out.println("synchronizedClass");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 用在类
*/
private void synchronizedGetClass() {
synchronized (this.getClass()) {
System.out.println("synchronizedGetClass");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这里的两种用法是同步块的用法,这里表示只有获取到这个类锁才能进入这个代码块。
4.同步this 实例方法
这也是同步块的用法,表示锁住整个当前对象实例,只有获取到这个实例的锁才能进入这个方法。
/**
* 用在this
*/
private void synchronizedThis() {
synchronized (this) {
System.out.println("synchronizedThis");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
用法和同步普通方法锁一样,都是锁住整个当前实例。
5. 同步对象实例方法
public void method3(SomeObject obj)
{
//obj 锁定的对象
synchronized(obj)
{
// todo
}
}
这也是同步块的用法,和上面的锁住当前实例一样,这里表示锁住整个 LOCK 对象实例,只有获取到这个 LOCK 实例的锁才能进入这个方法