一、基本概念
1、程序:是为了完成特定的任务,用某种语言编写的一组指令的集合。一段静态的代码。
2、进程:程序的一次执行过程,或是正在运行的程序。是一个动态的过程,自身的产生、存在和消亡。(生命周期)
3、线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。若一个进程同一时间并行执行多个线程,就是支持多线程的。线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器。线程的切换开销小。一个进程中的多个线程共享相同的内存单元,可以访问相同的变量和对象,每个线程都拥有自己的栈空间,共有同一个堆内存,使得线程间的通信更加的简便高效。但多线程共享系统资源可能会带来安全隐患。
3、单核cpu,其实就是一种假的线程,同一个时间内只能执行一个线程。例如收费站,有多个通道只有一个收费员。但是切换的频率高,感觉不出来。多核cpu,就是多线程。收费站,有多个通道和多个一个收费员。
4、并行:两个或多个事件在同一时刻发生
5、并发:两个或多个事件在同一时间段发生
6、使用多线程的优点:
提高应用程序的响应,对图形化界面更有意义。
提高cpu的利用率。
改善程序结构,将长而复杂的程序分为多个线程,独立运行。
7、为何需要多线程:
程序需要同时执行多个任务。
程序需要实现一些需要等待的任务时,如用户的输入、文件读写操作等。
需要后台运行程序时。
二、线程的创建和使用
1、线程的创建(两种方式)和启动:
Java的JVM允许程序运行多个线程,通过java.lang.Thread类来实现
Thread类的特性:每个线程都是通过某个特定Thread对象的run()方法来完成操作,run()方法的主体称为线程体.
通过该Thread对象的start() 方法来启动线程,而非直接调用run()方法。
2、Thread类中常用的方法:
stat():启动当前线程
run():重写该方法,将要执行的操作声明在方法中
currentThread():静态方法,返回当前的线程
getName():获取当前线程的名字
setName():设置当前线程的名字
yield():释放当前cpu的执行权,让其他的先走一会
join():线程A调用调用线程B的join(),让线程A进入阻塞状态,直到线程B执行完,才执行A
sleep(long millitime):让当前线程睡眠millitime时间
isAlive():判断当前线程是否还存活.
3、线程的调度,两种策略:
时间片:一段时间执行A,一段时间执行B
抢占式:优先级高的线程抢占CPU
4、线程的优先级:
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5(默认)
通过getPriority() 方法来获取当前线程的优先级,通过setPriority() 方法来设置优先级
注意:高优先级要抢占低优先级线程cpu的执行权,只是在概率上来讲,并不意外着高优先级执行完才是低优先级.
5、创建线程的第一种方式:
创建一个继承于Thread的子类,重写run方法(将执行操作写在里面),创建Thread对象然后调用start()方法
public class Main {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
System.out.println("111");//与上面的那个同时进行
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 == 0) System.out.println(i);
}
}
}
public class Main {
public static void main(String[] args) {
//匿名子类创建,同时进行两个线程
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 == 0) System.out.println(i);
}
}
}.start();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 != 0) System.out.println(i);
}
}
}.start();
}
}
6、创建线程的第二种方式:
创建一个实现Runnable的接口,实现类去实现Runnable中的抽象方法run,创建实现类的对象,将此对象作为形参传到Thread类的构造器中,创建Thread对象,然后调用start方法。(Thread也是Runnable接口的实现类)
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread);
t1.setName("线程1");
t1.start();
// 再启动一个线程
Thread t2 = new Thread(myThread);
t2.setName("线程2");
t2.start();
}
}
class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2==0) System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
实现Runnable接口的优势是:
1、通过创建任务,然后给线程分配的方式来实现的多线程,更适合多个线程的同时执行相同的任务情况。
2、可以避免单继承的局限性。
3、任务与线程本身是分离的,提高了程序的健壮性。
4、后继学习的线程池技术,只接收实现Runnable接口的任务。
小练习:
方式一:写三个窗口卖一百种票
public class Main {
public static void main(String[] args) {
window w1 = new window();
window w2 = new window();
window w3 = new window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
class window extends Thread{
private static int ticket = 100;//三个窗口总共卖100张,用静态属性
@Override
public void run() {
while (true){
if( ticket > 0){
System.out.println(Thread.currentThread().getName() + "卖出票:"+ticket);
ticket--;
}else{
break;
}
ticket--;
}
}
}
方式二:写三个窗口卖一百种票
public class Main {
public static void main(String[] args) {
window w = new window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class window implements Runnable{
private int ticket = 100;//不用设置成static,公用同一个对象
@Override
public void run() {
while (true){
if( ticket > 0){
System.out.println(Thread.currentThread().getName() + "卖出票:"+ticket);
ticket--;
}else{
break;
}
}
}
}
有线程安全的问题:比如会出现重票、错票等
7、中断线程
//主线程和t1线程同时执行任务,主线程先执行完后中断t1
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyThread());
t1.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+" :"+i);
Thread.sleep(500);
}
//当主线程执行完后,中断t1
t1.interrupt();//打个标记进入catch
}
static class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+" :"+i);
try {
Thread.sleep(1000);//只能进行try-catch,父接口没有声明异常抛出,子就不能声明更大的
} catch (InterruptedException e) {
//InterruptedException 线程中断异常,有wait、sleep、interrupt时会被捕获
//e.printStackTrace();
return;//结束线程,如果直接调用.stop 会导致一些应该被释放的资源没有被释放
}
}
}
}
}
8、守护线程
线程可以分为:
用户线程:当一个进程不包含任何存活的用户线程时,进程结束。
守护线程(Daemon):当最后一个用户线程结束时,所有守护线程自动死亡。
创建方法:
setDaemon(true),来设置线程为“守护线程”,其他非守护线程执行完后,该线程自动结束。
三、线程的安全性问题:
1、线程的安全性:
线程出现安全性的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句执行了部分,还没有执行完,另一个线程参与进来导致共享数据的错误。
解决办法:对多个操作共享数据的语句,只能让一个线程都执行完了才能执行其他线程。
2、解决方法一:同步代码块
synchronized (同步监视器/锁对象) {//每个线程进来时,都会判断这个锁对象是否上锁
//需要被同步的代码
}
1、操作共享数据的代码,即为需要被同步的代码(不能多,不能少)
2、同步监视器,俗称“锁”。任何一个类的对象都可以作为锁。多线程必须要共用一把锁
继承的时候
class window extends Thread{
private static int ticket = 100;
private static Object obj = new Object();//设置为static,保证为同一把锁
@Override
public void run() {
while (true){
synchronized (obj) {
if( ticket > 0){
System.out.println(Thread.currentThread().getName() + "卖出票:"+ticket);
ticket--;
}else{
break;
}
ticket--;
}
}
}
}
3、解决方法二:同步方法
1、仍然需要锁
2、非静态的同步方法,同步监视器是:this
3、静态的同步方法,同步监视器是:类本身
public void synchronized 方法名(){
//方法的内容
}
在用继承的方式,要考虑方法是否需要加static
3、解决方法二:显式定义同步锁。
对比synchronized,需要手动解锁
class window implements Runnable{
private int ticket = 100;
//造一个lock对象(静态,保证相同)
private static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try{
lock.lock();//锁住
if( ticket > 0){
System.out.println(Thread.currentThread().getName() + "卖出票:"+ticket);
ticket--;
}else{
break;
}
}finally {
lock.unlock();//开锁
}
}
}
}
5、公平锁和非公平锁
公平锁:当要等待线程的时候,等待的线程会依次排队,先到先得。
非公平锁:线程不会排队,直接抢。
上面说的显示锁和隐式锁默认都是非公平锁,但是显示锁可以设置为公平锁,只需要在创建对象的时候,设置ReentrantLock lock = new ReentrantLock(true);
4、应用:解决单例模式中的懒汉式的线程安全问题:
class Bank{
private Bank(){}
private static Bank instance = null;
public static Bank getInstance(){
if( instance == null) instance = new Bank();
return instance;
}
}
可能有多个线程进入getInstance中,造成new多个Bank
同步方法:
class Bank{
private Bank(){}
private static Bank instance = null;
public static synchronized Bank getInstance(){
if( instance == null) instance = new Bank();
return instance;
}
}
同步代码块:
class Bank{
private Bank(){}
private static Bank instance = null;
public static Bank getInstance(){
//方式一:效率低
synchronized (Bank.class) {
if( instance == null) instance = new Bank();
return instance;
}
//方式二:
if(instance == null){
synchronized (Bank.class) {
if( instance == null) instance = new Bank();
return instance;
}
}
return instance;
}
}
5、死锁:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁。
public class Main {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append("1");
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//在它阻塞的0.1s内,下面的那个拿着s2等着要s1,而这个线程拿着s1,等着要s2,这样就形成了死锁
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}