Echking’s Wiki·不定期更新学习记录，愿其能促我在这条路上不断进步，与我一起欣赏代码的魅力吧，希望你能在这里找到对你有用的东西。Hello World！
分析源码在BasicErrorController源码中的errorHtml方法中@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = Collections .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView != null) ? modelAndView : new ModelAndView("error", model); }会返回一个ModelAndView，其中包含请求信息，响应信息，状态码，和模型数据。在ModelAndView modelAndView = resolveErrorView(request, response, status, model);中的resolveErrorView方法为：protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) { for (ErrorViewResolver resolver : this.errorViewResolvers) { ModelAndView modelAndView = resolver.resolveErrorView(request, status, model); if (modelAndView != null) { return modelAndView; } } return null; } }它会遍历所有的errorViewResolvers，找到一个匹配的返回。在其中的ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);接口resolveErrorView，其实现在DefaultErrorViewResolver中：@Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { ModelAndView modelAndView = resolve(String.valueOf(status.value()), model); if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView; }该方法中的resolve(String.valueOf(status.value()), model);具体方法为：private ModelAndView resolve(String viewName, Map<String, Object> model) { String errorViewName = "error/" + viewName; TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext); if (provider != null) { return new ModelAndView(errorViewName, model); } return resolveResource(errorViewName, model); }其要求当前传过来的错误状态码，会拼接在error/下，所以错误页面资源必须在error文件夹下才能被识别到。由该方法可知，其会优先精准匹配error文件夹下的错误页面（404.html，500.html等），然后返回该页面（ return resolveResource(errorViewName, model);）；如果没有则会使用SERIES_VIEWS.containsKey(status.series())再去查找系列错误状态码，例如4xx.html，5xx.htmlstatic { Map<Series, String> views = new EnumMap<>(Series.class); views.put(Series.CLIENT_ERROR, "4xx"); views.put(Series.SERVER_ERROR, "5xx"); SERIES_VIEWS = Collections.unmodifiableMap(views); }然后调用resolve方法拼接。最后调用resolveResource`方法，寻找资源，若找到则返回，否则返回null。在BasicErrorController中的：ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);可以看出如果没有匹配到自定义的异常页面，则会直接返回new ModelAndView("error", model);即默认error视图，也就是：image-20230426203231308添加错误页面image-20230426213724504发送一个错误请求：image-20230426213825474可以看到，错误页面已经生效了。但如果有除了404和500以外的错误，还是会返回默认错误页，例如400错误：image-20230426214229914这时可以添加4xx.html和5xx.html页面。image-20230426214412241这时400错误也跳到了自定义错误页面image-20230426214512325定制错误页面在自动配置类ErrorMvcAutoConfiguration中有一个：@Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(); }DefaultErrorAttributes中存储了一些默认属性。而在BasicErrorController中调用了多次getErrorAttributes()获取错误属性的方法。而getErrorAttributes()又调用了：return this.errorAttributes.getErrorAttributes(webRequest, options);default Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { return Collections.emptyMap(); }接着找到：@Override public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE)); if (!options.isIncluded(Include.EXCEPTION)) { errorAttributes.remove("exception"); } if (!options.isIncluded(Include.STACK_TRACE)) { errorAttributes.remove("trace"); } if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) { errorAttributes.remove("message"); } if (!options.isIncluded(Include.BINDING_ERRORS)) { errorAttributes.remove("errors"); } return errorAttributes; }又调用了方法getErrorAttributes()，在该方法中：private Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { Map<String, Object> errorAttributes = new LinkedHashMap<>(); errorAttributes.put("timestamp", new Date()); addStatus(errorAttributes, webRequest); addErrorDetails(errorAttributes, webRequest, includeStackTrace); addPath(errorAttributes, webRequest); return errorAttributes; }添加时间戳errorAttributes.put("timestamp", new Date());添加错误状态码和错误信息addStatus()方法中：errorAttributes.put("status", status);errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());添加异常信息 addErrorDetails()方法中：errorAttributes.put("exception", error.getClass().getName());(多层调用)errorAttributes.put("message", getMessage(webRequest, error));添加错误请求的路径addPath()方法中：errorAttributes.put("path", path);所以在页面上可以获取到timestamp,status,error,exception,message这几个数据。在原有页面中添加thymeleaf标签： 将 page not found We Couldn’t Find This Page Back To Home 修改为： We Couldn’t Find This Page We Couldn’t Find This Page We Couldn’t Find This Page We Couldn’t Find This Page We Couldn’t Find This Page Back To Home 访问错误页：image-20230426224246907image-20230426224344700可以看到除了message之外其他数据都拿到了。在自动配置类@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class }) public class ErrorMvcAutoConfiguration { ...... }中ServerProperties.class下private final ErrorProperties error = new ErrorProperties();可以看到：/** * When to include "message" attribute. */ private IncludeAttribute includeMessage = IncludeAttribute.NEVER;public enum IncludeAttribute { /** * Never add error attribute. */ NEVER, ...... }所以看不到message信息。需要去配置文件中修改该配置才能获取到message信息，如下#定制错误页信息 server.error.include-exception=true server.error.include-message=always再次访问错误页面：image-20230426225347153可以看到message已经正确获取到。
默认的异常页浏览器客户端 发生error500报错 (text/html页面)image-20230426184115683404报错 (text/html页面)image-20230426184212061Postman 报错500报错 (Json数据)image-20230426200911775404报错 (Json数据)image-20230426200946513源码org.springframework.boot.autoconfigure -> web -> servlet -> error -> ErrorMvcAutoConfiguration在配置类ErrorMvcAutoConfiguration中，由其注解可知@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class })可以在配置文件中通过其前缀来修改该异常配置类的配置。该配置类中的两个Bean可以看出，第一个errorAttributes为错误属性；basicErrorController为控制器，来处理请求。@Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(); } @Bean @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) public BasicErrorController basicErrorController(ErrorAttributes errorAttributes, ObjectProvider<ErrorViewResolver> errorViewResolvers) { return new BasicErrorController(errorAttributes, this.serverProperties.getError(), errorViewResolvers.orderedStream().collect(Collectors.toList())); }在BasicErrorController中，可以看到使用了注解@Controller和@RequestMapping@Controller @RequestMapping("${server.error.path:${error.path:/error}}")而在之前的错误信息中，发送的请求是/error所以BasicErrorController就是来处理/error请求的。BasicErrorController中标注了@RequestMapping的方法，第一个（用于浏览器端）：image-20230426202435448第二个（Psotman机器客户端）：image-20230426202518337在ErrorMvcAutoConfiguration的源码中，类WhitelabelErrorViewConfiguration中配置了默认错误页面。protected static class WhitelabelErrorViewConfiguration { ...... }image-20230426203231308当发起错误请求，没有自定义异常来处理，则由BasicErrorController来处理，会检测媒体类型，如果是html页面，没有自定义的错误页面，就会使用默认配置的html页面；若是Postman等机器客户端发起的错误请求，则响应一个Json数据(return new ResponseEntity<>(body,status))。
//跳转至login.html页面 @GetMapping("/login") public String goToLogin(){ return "login" }Thymeleaf是如何将String字符串"login"解析成"classpath:/templates/login.html"的？一、前端控制器DispatcherServlet前端控制器中的doDispacth在处理请求时，渲染页面过程：先通过//DispatcherServlet.class 1043行 mappedhandler = getHandler(processedrequest)查找url是否存在，若存在则返回执行链和适配器，在//DispatcherServlet.class 1067行 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());真正执行Handler然后在//DispatcherServlet.class 1084行 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);渲染视图，而Thymeleaf提供了视图解析器，所以在这里渲染视图的是Thymeleaf的视图解析器。二、ThymeleafAutoConfiguration在ThymeleafAutoConfiguration中，SpringBoot2 就已经为我们默认注册了一个thymeleafViewResolver@Bean @ConditionalOnMissingBean(name = "thymeleafViewResolver") ThymeleafViewResolver thymeleafViewResolver(ThymeleafProperties properties, SpringTemplateEngine templateEngine) { ...... return resolver; }只要没有手动自己添加，则会默认使用该视图解析器。在ThymeleafViewResolver中的createView创建视图方法，会传入一个视图名称，如下代码所示的viewName@Override protected View createView(final String viewName, final Locale locale) throws Exception { ...... }然后通过viewName.startWith()方法截取// Process redirects (HTTP redirects) //此处的REDIRECT_URL_PREFIX的值为： //public static final String REDIRECT_URL_PREFIX = "redirect:"; if (viewName.startsWith(REDIRECT_URL_PREFIX)) { vrlogger.trace("[THYMELEAF] View \"{}\" is a redirect, and will not be handled directly by ThymeleafViewResolver.", viewName); final String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length(), viewName.length()); final RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible()); return (View) getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, REDIRECT_URL_PREFIX); }然后通过RedirectView构建视图返回。forward前缀同理。如果前缀不符合上述两种，则执行return loadView(viewName, locale);@Override protected View loadView(final String viewName, final Locale locale) throws Exception { ...... }利用loadView最终实例化view = (AbstractThymeleafView) beanFactory.initializeBean(viewInstance, viewName);通过AbstractThymeleafView将例如"login"拼接完成。
SpringBoot2 底层注解@Configuration与@Component同作用，但能让其他人更方便地识别为配置类；该注解源码中有一行代码：boolean proxyBeanMethods() default true;1.自定义的配置类，相当于xml配置文件，配置类本身也是组件2.配置类里面使用@Bean标注在方法上给容器注册组件，默认也是单实例的3.属性proxyBeanMethodsFull模式：默认是true 用来监测当前IOC容器中是否有此实例，若有则不会再创建，直接返回此单实例；当前配置类的实例信息：com.example.springboot.config.MyConfig1$$EnhancerBySpringCGLIB$$7c35a8af@aa21042 代理对象Lite模式：指定为false 每一次调用都会创建新的实例；当前配置类的实例信息：com.example.springboot.config.MyConfig1@45673f68使用场景总结：Lite模式，指定proxyBeanMethods值为false，配置类组件之间无依赖关系用Lite模式加速容器启动过程，减少判断配置Full模式，默认指定proxyBeanMethods值为true，类组件之间有依赖关系，方法会被调用得到之前单实例组件注册实例bean至IOC容器中的方法方式一@Configuration + @Bean会将添加了@Bean注解的方法的返回值注册到IOC容器中以方法明作为beanName存在。方式二@Configuration + @Import( {xxx.class})将指定的class对象注册至IOC容器中，以全类名作为beanName存在。全类名：包名.类名方式三@Configuration + @ImportResource( {"classpath:xxx.xml"})将xml配置文件中的实例bean，注册到容器中，翻新SpringMVC项目时常用。@Import( { })将指定的class对象注册至IOC容器中，以全类名作为beanName存在。@Conditional一般称为条件装配。分为：@ConditionalONXxx 表示一定具备需要提供的内容，此处才生效@ConditionalMissingXxx 表示一定没有提供此内容，此配置才生效例如：@ConditionalOnBean , @ConditionalMissingBean实例代码：@Configuration //一旦IOC容器中未提供Car实例，则此处MyConfig生效 @ConditionalOnMissingBean(Car.class) public class MyConfig(){ }@Configuration //一旦IOC容器中提供Car实例，则此处MyConfig生效 @ConditionalOnClass(Car.class) public class MyConfig(){ }SpringBoot 2 底层源码+主启动类部分分析image-20230423203717288 此处内容已隐藏，回复后可见 此处内容已隐藏，回复后可见
RedisAutoConfiguration（Redis场景定制）@AutoConfiguration @ConditionalOnClass({RedisOperations.class}) @EnableConfigurationProperties({RedisProperties.class}) @Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class}) public class RedisAutoConfiguration { public RedisAutoConfiguration() { } @Bean @ConditionalOnMissingBean( name = {"redisTemplate"} ) @ConditionalOnSingleCandidate(RedisConnectionFactory.class) public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean @ConditionalOnSingleCandidate(RedisConnectionFactory.class) public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { return new StringRedisTemplate(redisConnectionFactory); } }RedisAutoConfiguration类上的注解一、@AutoConfiguration自动配置类注解是一个组合注解，组合中的注解有：@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration( proxyBeanMethods = false ) @AutoConfigureBefore @AutoConfigureAfter前三个元注解@Target( {ElementType.TYPE})该注解说明了注解修饰的对象范围，也就是注解可以使用的地方。其取值有：1、CONSTRUCTOR:用于描述构造器2、FIELD:用于描述域3、LOCAL_VARIABLE:用于描述局部变量4、METHOD:用于描述方法5、PACKAGE:用于描述包6、PARAMETER:用于描述参数7、TYPE:用于描述类、接口(包括注解类型) 或enum声明@Retention(RetentionPolicy.RUNTIME)该注解用来修饰注解的生命周期。其取值有：1、SOURCE：注解只保留在源文件，当Java文件编译成class文件的时候，注解被遗弃；2、CLASS：注解被保留到class文件，但jvm加载class文件时候被遗弃，这是默认的生命周期；3、RUNTIME：注解不仅被保存到class文件中，jvm加载class文件之后，仍然存在；@Documented该注解用于生成JavaDoc文档。其他三个注解@Configuration(proxyBeanMethods = false)该注解为配置类注解，是@Component注解的扩展注解，相当于xml配置文件，配置类本身也是组件。而且在这里使用的是轻加载。@AutoConfigureBefore一般配合@Component注解使用，表明在某类加载后再执行该配置类@AutoConfigureAfter表明在某类加载之前执行该配置类二、@ConditionalOnClass( {RedisOperations.class})该注解下有四个注解，前三个是元注解。@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional({OnClassCondition.class})@Conditional( {OnClassCondition.class})该注解为条件装配，按照设定的条件进行判断，把满足判断条件的bean注册到Spring容器。所以该注解@ConditionalOnClass({RedisOperations.class})意为当项目中存在RedisOperations类时，该注解下的RedisAutoConfiguration类才会生效三、@EnableConfigurationProperties( {RedisProperties.class})该注解下有四个注解@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Import({EnableConfigurationPropertiesRegistrar.class})其中前三个为元注解。@Import( {EnableConfigurationPropertiesRegistrar.class})该注解用于导入组件。@EnableConfigurationProperties({RedisProperties.class}) 意为使使用了@ConfigurationProperties()的类生效，本处就是使RedisProperties类生效。RedisProperties类中确实使用了@ConfigurationPropertie()@ConfigurationProperties( prefix = "spring.redis" ) public class RedisProperties { .....RedisAutoConfiguration类中方法上的注解@Bean @ConditionalOnMissingBean( name = {"redisTemplate"} ) ...... @ConditionalOnSingleCandidate(RedisConnectionFactory.class) ...... @Bean @ConditionalOnMissingBean @ConditionalOnSingleCandidate(RedisConnectionFactory.class)@Bean注册bean对象。声明当前方法的返回值是一个 Bean。@ConditionalOnMissingBean(name = {"redisTemplate"})修饰bean的一个注解，当bean被注册到IOC容器后之后，如果又注册了相同类型的bean，就不会成功，它会保证你的bean只有一个，即你的实例只有一个。@ConditionalOnSingleCandidate(RedisConnectionFactory.class)表示当指定的RedisConnectionFactory类已注册到容器中，且是单例时则将该注解下的Bean注入到IOC容器中。image-20230422190612110
权限设置问题http.authorizeRequests() //放行login .antMatchers("/checkLogin","/login.html").permitAll() .antMatchers("/pages/admin/**").hasRole("人事部主任") .antMatchers("/pages/bmAdmin/**").hasRole("部门主任") .antMatchers("/pages/emp/**").hasRole("部门员工") .antMatchers("/pages/renshi/**").hasRole("人事部员工");按上面设置后，项目接口可以无权限访问，需要添加一条.anyRequest().authenticated();所有请求都需要具有角色权限。当项目加入权限控制后，所有请求都会走一遍Spring Security，但如果当前发送请求的对象没有登陆时，会返回到设置的.failureForwardUrl("/index.html");上述路径中。如果已经登录，但无此请求所需权限，则会直接跳转到设置的403页面中。
IDEA Debug 功能image-20220831144818923Show Execution Point：回到当前加载程序断点位置；Step Over：从当前断点逐步执行代码（运行到方法时直接跳过）；Step Into：加载测试方法并跳转到测试方法内部执行测试方法内部代码块，但是不会进入java类库的代码块；Force Step Into：但是不同于Step into 按钮的是当前 Force Step Into 按钮除了可以向下执行的同时，也能进入自定义方法和进入到java类库方法内部；Step Out：回退，每次回退都会回退到上层被调用的方法下方法；Run to Cursor：点击后，不进入断点；Evaluate Expression：计算器按钮，可以实现在不运行完成所有DeBug的情况下，实现数据运算，提前查看结果。
笔记部分整理自 尚学堂.百战程序员 视频链接进程和线程的区别线程在进程中运行的。一个进程可以包含多个线程。不同进程间数据很难共享，而同一进程下不同线程间数据很易共享。进程要比线程消耗更多的计算机资源。进程间不会相互影响，因为它们的空间是完全隔离的。而进程中的一个线程挂掉将导致整个进程挂掉。进程使用的内存地址可以上锁，即一个线程使用某些共享内存时，其他线程必须等待结束才能使用这一块内存。线程的创建通过继承Thread类实现多线程在Java中负责实现线程功能的类是java.lang.Thread 类。继承Thread类实现多线程的步骤:继承Thread类定义线程类。重写Thread类中的run()方法。run()方法也称为线程体。实例化线程类并通过start()方法启动线程。子线程要在主线程中启动public class TestThread extends Thread{ public static void main(String[] args){ System.out.println("主线程开始"); //实例化线程类 TestThread t1 = new TestThread(); TestThread t2 = new TestThread(); //启动线程 //如果直接写t1.run(); 那么就在主线程中调用run方法 //所以我们需要使用start方法启动 t1.start(); t2.start(); //两个线程并发 //此时有三个线程：主线程和两个子线程 //程序运行->实例化线程类->子线程启动->主线程结束->子线程工作->程序结束 System.out.println("主线程结束"); } public TestThread(){ System.out.println(this.getName()); } //线程的线程体 @Override public void run(){ System.out.println(this.getName()+"线程开始"); for(int i=0;i<20;i++){ System.out.println(this.getName()+i); } System.out.println(this.getName()+"线程结束"); } }通过实现Runnable接口实现多线程public class TestThread implements Runnable{ public static void main(String[] args){ System.out.println("主线程开始"); TestThread t1 = new TestThread(); //启动线程 Thread t = new Thread(t1); t.start(); System.out.println("主线程结束"); } public TestThread(){ //这里会输出主线程名称 System.out.println(Thread.currentThread().getName()); } //线程的线程体 @Override public void run(){ System.out.println(Thread.currentThread().getName()+"线程开始"); for(int i=0;i<20;i++ ){ System.out.println(Thread.currentThread().getName()+" "+i); } System.out.println(Thread.currentThread().getName()+"线程结束"); } }线程的执行流程image-20220923204932996线程的生命周期image-20220923222647384一个线程对象在它的生命周期内，需要经历5个状态。新生状态(New)用new关键字建立一个线程对象后，该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间，通过调用start方法进入就绪状态。就绪状态(Runnable)处于就绪状态的线程已经具备了运行条件，但是还没有被分配到CPU，处于“线程就绪队列”，等待系统为其分配CPU。就绪状态并不是执行状态，当系统选定一个等待执行的Thread对象后，它就会进入执行状态。一旦获得CPU，线程就进入运行状态并自动调用自己的run方法。有四种原因会导致线程进入就绪状态:新建线程：调用start()方法，进入就绪状态;阻塞线程：阻塞解除,进入就绪状态;运行线程：调用yield()方法，直接进入就绪状态;运行线程：JVM将CPU资源从本线程切换到其他线程。运行状态(Running)在运行状态的线程执行自己run方法中的代码，直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片没有执行结束，就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。阻塞状态(Blocked)阻塞指的是暂停一个线程的执行以等待某个条件发生（如某资源就绪)。有4种原因会导致阻塞:执行sleep(int millisecond)方法，使当前线程休眠，进入阻塞状态。当指定的时间到了后，线程进入就绪状态。执行wait()方法，使当前线程进入阻塞状态。当使用nofity()方法唤醒这个线程后，它进入就绪状态。线程运行时，某个操作进入阻塞状态，比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后，线程进入就绪状态。join()线程联合:当某个线程等待另一个线程执行结束后，才能继续执行时，使用join()方法。死亡状态(Terminated)死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它run()方法内的全部工作；另一个是线程被强制终止，如通过执行stop()或destroy()方法来终止一个线程（注: stop()/destroy()方法已经被JDK废弃，不推荐使用）当一个线程进入死亡状态后，就不能回到其他状态了。线程的使用终止线程如果我们想在一个线程中终止另一个线程我们一般不使用JDK提供的stop()/destroy()方法(它们本身也被JDK废弃了)。通常的做是提供一个boolean型的终止变量，当这个变量值为false时，则终止线程的运行。public class StopThread implements Runnable{ public static void main(String[] args) throws Exception{ System.out.println("主线程开始"); StopThread st = new StopThread(); Thread t1 = new Thread(st); t1.start(); //使主线程阻塞，键盘输入后解除 System.in.read(); st.stop(); System.out.println("主线程开始"); } private boolean flag = true; @Override public void run(){ System.out.println(Thread.currentThread().getName()+"线程开始"); int i = 0; while(flag){ System.out.println(Thread.currentThread().getName()+" "+i); try{ Thread.sleep(200); }catch (InterruptedException e){ e.printStackTrace(); } } //这个代码段使用的终止线程方法，会将下方的输出代码也执行 System.out.println(Thread.currentThread().getName()+"线程结束"); } public void stop(){ this.flag = false; } }暂停线程暂停当前线程执行sleep()/yield()sleep()方法暂停线程执行常用的方法有sleep()和yield()方法，这两个方法的区别是:sleep()方法:可以让正在运行的线程进入阻塞状态，直到休眠时间满了，进入就绪状态。yield()方法:可以让正在运行的线程直接进入就绪状态，让出CPU的使用权。public class SleepThread implements Runnable{ public static void main(String[] args){ System.out.println("主线程开始"); Thread t = new Thread(new SleepThread()); t.start(); System.out.println("主线程结束"); } @Override public void run(){ System.out.println(Thread.currentThread().getName()+"线程开始"); for(int i=0;i<20;i++){ System.out.println(Thread.currentThread().getName()); try{ Thread.sleep(1000); }catch (InterruptedException e){ e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+"线程结束"); } }yeild()方法yield()方法的作用:暂停当前正在执行的线程，并执行其他线程。yield()让当前正在运行的线程回到可运行状态，以允许具有相同优先级的其他线程获得运行的机会因此，使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是，实际中无法保证`yield()达到让步的目的，因为，让步的线程可能被线程调度程序再次选中。使用yield方法时要注意的几点:yield是一个静态的方法。调用yield后，yield告诉当前线程把运行机会交给具有相同优先级的线程。yield不能保证，当前线程迅速从运行状态切换到就绪状态。yield只能是将当前线程从运行状态转换到就绪状态，而不能是等待或者阻塞状态。public class YeildThread implements Runnable{ public static void main(String[] args){ Thread t1 = new Thread(new YeildThread()); Thread t2 = new Thread(new YeildThread()); t1.start(); t2.start(); } @Override public void run(){ for(int i=0;i<30;i++){ if("Thread-0".equals(Thread.currentThread().getName())){ if(i==0){ //让步可能失败 Thread.yield(); } } System.out.println(Thread.currentThread().getName()+" "+i); } } }线程的联合当前线程邀请调用方法的线程优先执行，在调用方法的线程执行结束之前，当前线程不能再次执行。线程A在运行期间，可以调用线程B的join()方法，让线程B和线程A联合。这样，线程A就必须等待线程B执行完毕后，才能继续执行（类似于方法调用，在A方法中调用B方法后，A方法会在B方法执行完后继续执行）。join()方法的使用class A implements Runnable{ @Override public void run(){ for(int i=0;i<10;i++){ System.out.println(Thread.currentThread().getName()+" "+i); try{ Thread.sleep(1000); }catch (InterruptedException e){ e.printStackTrace(); } } } } public class JoinThread{ public static void main(String[] args){ Thread t = new Thread(new A()); //并行子线程A //t.start(); //主线程main for(int i=0;i<10;i++){ System.out.println(Thread.currentThread().getName()+" "+i); if(i==2){ try{ //通过当前线程对象调用join方法进行联合 t.join(); }catch(InterruptedException e){ e.printStackTrace(); } } try{ Thread.sleep(1000); }catch (InterruptedException e){ e.printStackTrace(); } } } }若此时还有一个B线程，没有联合。当主线程和A线程联合后，主线程阻塞，A线程和B线程并行运行。线程联合案例案例：爸爸让儿子去买烟，儿子买回来，爸爸再去抽烟。分析：共需要三个线程：主线程，爸爸抽烟的线程，儿子买烟的线程public class JoinDemo{ public static void main(String[] args){ Thread t = new Thread(new FatherThread()); //启动爸爸抽烟的线程 t.start(); } } //爸爸抽烟的线程 class FatherThread implements Runnable{ @Override public void run(){ System.out.println("爸爸想抽烟，发现烟抽完了。"); System.out.println("爸爸让儿子去买一包烟。"); //线程联合 //启动儿子买烟的线程 Thread t = new Thread(new SonThread()); t.start(); System.out.println("爸爸等待儿子买烟回来。"); try{ t.join(); }catch (InterruptedException e){ e.printStackTrace(); System.out.println("爸爸出门找儿子"); System.exit(0); } System.out.println("爸爸拿到烟，继续抽烟。"); } } //儿子买烟的线程 class SonThread implements Runnable{ @Override public void run(){ System.out.println("给爸爸去买烟。"); System.out.println("儿子买烟要花时间。"); for(int i=0;i<10;i++){ System.out.println("第"+i+"分钟"); try{ Thread.sleep(2000); }catch (InterruptedException e){ e.printStackTrace(); } } } }打印结果爸爸想抽烟，发现烟抽完了。爸爸让儿子去买一包烟。爸爸等待儿子买烟回来。给爸爸去买烟。儿子买烟要花时间。第0分钟第1分钟第2分钟第3分钟第4分钟第5分钟第6分钟第7分钟第8分钟第9分钟爸爸拿到烟，继续抽烟。Thread类中的其他常用方法getName()获取当前线程名称方式一（适用于通过继承Thread类实现的多线程）this.getName()获取线程名称，该方法适用于继承Thread 实现多线程方式class GetName1 extends Thread{ @Override public void run(){ //因为GetName1是直接继承了Thread，所以可以直接调用Thread中的公有方法getName() System.out.println(this.getName()); } } public class GetNameThread{ public static void main (String[] args){ GetName1 getName1 = new GetName1(); getName1.start(); } }方式二（适用于实现Runnable接口实现多线程）Thread.currentThread().getName()获取线程名称，该方法适用于实现Runnable接口实现多线程方式。class GetName2 implements Runnable{ @Override public void run(){ //线程类实现了Runnable接口，那么线程类无法直接拿到getName方法 //Thread.currentThread()是一个静态方法，返回当前线程对象 System.out.println(Thread.currentThread().getName()); } } public class GetNameThread{ public static void main (String[] args){ Thread t = new Thread(new GetName2()); t.start(); } }setName()设置线程的名称方式一（适用于通过继承Thread类实现的多线程）通过构造方法设置线程名称。class SetName1 extends Thread{ //Thread中含有多个有参和无参构造方法 public SetName1(String name){ //将设定的名称传递到Thread中 super(name); } @Override public void run(){ System.out.println(this.getName()); } } public class SetNameThread{ public static void main(String[] args){ SetName1 setName1 = new SetName1("SetName1"); setName1.setName("SetName1-test") setName1.start(); } }方式二（适用于实现Runnable接口实现多线程）通过setName()方法设置线程名称。class SetName2 implements Runnable{ //Thread中含有多个有参和无参构造方法 public SetName2(String name){ //将设定的名称传递到Thread中 super(name); } @Override public void run(){ System.out.println(Thread.currentThread().getName()); } } public class SetNameThread{ public static void main(String[] args){ Thread t = new Thread(new SetName2()); thread.setName("SetName2"); thread.start(); } }isAlive()判断当前线程是否存活isAlive()方法:判断当前的线程是否处于活动状态。活动状态是指线程已经启动且尚未终止，线程处于正在运行或准备开始运行的状态，就认为线程是存活的。class Alive implements Runnable{ @Override public void run(){ System.out.println("第二次:"+Thread.currentThread().isAlive()); try{ Thread.sleep(20000); }catch (InterruptedException e){ e.printStackTrace(); } } } public class AliveThread{ public static void main(String[] args){ Thread thread = new Thread(new Alive()); System.out.println("第一次:"+thread.isAlive()); thread.start(); System.out.println("第三次:"+thread.isAlive()); try{ Thread.sleep(1000); }catch (InterruptedException e){ e.printStackTrace(); } System.out.println("第四次:"+thread.isAlive()); } }运行结果：第一次:false第三次:true第二次:true第四次:true线程的优先级每一个线程都是有优先级的，我们可以为每个线程定义线程的优先级，但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程的优先级用数字表示，范围从1到10，一个线程的缺省优先级是5。Java 的线程优先级调度会委托给操作系统去处理，所以与具体的操作系统优先级有关，如非特别需要，一般无需设置线程优先级。注意:线程的优先级，不是说哪个线程优先执行，如果设置某个线程的优先级高。那就是有可能被执行的概率高。并不是优先执行。线程优先级的使用使用下列方法获得或设置线程对象的优先级int getPriority()void setPriority(int newPriority)setPriority()设置线程优先级class Priority implements Runnable { private int num = 0; private boolean flag = true; @Override public void run() { while (this.flag) { System.out.println(Thread.currentThread().getName() + " " + num++); } } public void stop () { this.flag = false; } } public class PriorityThread{ public static void main(String[] args)throws Exception{ Priority p1 = new Priority(); Priority p2 = new Priority(); Thread t1 = new Thread(p1,"线程1"); Thread t2 = new Thread(p2,"线程2"); //Thread.MAX_PRIORITY 最高优先级10 t1.setPriority(Thread.MAX_PRIORITY); //Thread.MIN_PRIORITY 最低优先级1 t2.setPriority(Thread.MIN_PRIORITY); //启动线程 t1.start(); t2.start(); Thread.sleep(1000); p1.stop(); p2.stop(); } }守护线程在Java中有两类线程:User Thread(用户线程):就是应用程序里的自定义线程。Daemon Thread(守护线程):比如垃圾回收线程，就是最典型的守护线程。守护线程（即Daemon Thread)，是一个服务线程，准确地来说就是服务其他的线程，这是它的作用，而其他的线程只有一种，那就是用户线程。守护线程特点：守护线程会随着用户线程死亡而死亡。守护线程和用户线程的区别用户线程，不随主线程的死亡而死亡。用户线程只有两种情况会死亡，一种是在run中异常终止，另一种是把run执行完毕，线程死亡。守护线程，随着用户线程的死亡而死亡，当用户线程死亡守护线程也会随之死亡。setDaemon()添加守护线程class Daemon implements Runnable{ @Override public void run() { for (int i = 0; i <20; i++) { System.out.println(Thread.currentThread().getName()+" "+i); try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class DaemonThread { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(new Daemon(),"Daemon"); //将该线程设置为守护线程 //此时是在主线程中设置，所以该守护线程为主线程服务 t.setDaemon(true); t.start(); Thread.sleep(1000); System.out.println("主线程结束"); } }打印结果：Daemon 0主线程结束守护线程只打印了一次i，就随主线程的死亡而死亡。线程同步什么是线程同步处理多线程问题时，多个线程访问同一个对象，并且某些线程还想修改这个对象。这时候，我们就需要用到“线程同步”。线程同步其实就是一种等待机制，多个需要同时访问此对象的线程进入这个对象的等待池形成队列，等待前面的线程使用完毕后，下一个线程再使用。线程冲突案例银行取钱的基本流程基本上可以分为如下几个步骤。用户输入账户、密码，系统判断用户的账户、密码是否匹配。用户输入取款金额。系统判断账户余额是否大于取款金额。如果余额大于取款金额，则取钱成功;如果余额小于取款金额，则取钱失败。class Account{ private String accountNo; private double balance; public Account() { } public Account(String accountNo, double balance) { this.accountNo = accountNo; this.balance = balance; } public String getAccountNo() { return accountNo; } public void setAccountNo(String accountNo) { this.accountNo = accountNo; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } } //取钱线程 class DrawThread extends Thread{ //账户对象 private Account account; private double drawMoney; public DrawThread(String name,Account account,double drawMoney){ //调用父类Thread构造方法，为线程命名 super(name); this.account=account; this.drawMoney=drawMoney; } @Override public void run(){ //判断当前账户余额 if(this.account.getBalance()>=this.drawMoney){ System.out.println(this.getName()+"取钱成功,"+this.drawMoney); //线程休眠 try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } this.account.setBalance(this.account.getBalance()-this.drawMoney); System.out.println("余额："+this.account.getBalance()); }else{ System.out.println(this.getName()+"余额不足"); } } } public class DrawMoneyThread { public static void main(String[] args) { Account account=new Account("Jack",2000); new DrawThread("小明",account,1000).start(); new DrawThread("小花",account,1500).start(); } }打印结果：小花取钱成功,2000.0小明取钱成功,1000.0余额：-1000.0余额：0.0线程冲突，前一个人没有将取的钱从余额扣除，后一个人就又取了钱，造成余额变成负数。实现线程同步synchronized关键字由于同一进程的多个线程共享同一块存储空间，在带来方便的同时，也带来了访问冲突的问题。Java语言提供了专门机制以解决这种冲突，有效避免了同一个数据对象被多个线程同时访问造成的这种问题。这套机制就是synchronized关键字。synchronized语法结构:synchronized(锁对象){ 同步代码 }synchronized 关键字使用时需要考虑的问题:需要对哪部分的代码在执行时具有线程互斥的能力(线程互斥:并行变串行)。需要对哪些线程中的代码具有互斥能力(通过synchronized锁对象来决定)。synchronized方法通过在方法声明中加入synchronized关键字来声明，语法如下:public synchronized void accessVal(int newVal);synchronized在方法声明时使用:放在范围操作符(public)之后,返回类型声明(void)之前。这时同一个对象下synchronized方法在多线程中执行时，该方法是同步的，即一次只能有一个线程进入该方法，其他线程要想在此时调用该方法，只能排队等候，当前线程(就是在synchronized方法内部的线程)执行完该方法后，别的线程才能进入。synchronized块synchronized方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。Java为我们提供了更好的解决办法，那就是synchronized 块。块可以让我们精确地控制到具体的“成员变量”，缩小同步的范围，提高效率。对取钱案例采用线程同步方法完善分析：线程同步：取款线程将账户作为线程锁对象，当两个人同时对一个账户操作时，会让先运行的线程进行操作，另一个线程进入阻塞，待第一个线程取完钱，对余额操作完后，第二个线程才进入取钱过程。class Account{ private String accountNo; private double balance; public Account() { } public Account(String accountNo, double balance) { this.accountNo = accountNo; this.balance = balance; } public String getAccountNo() { return accountNo; } public void setAccountNo(String accountNo) { this.accountNo = accountNo; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } } //取钱线程 class DrawThread extends Thread{ //账户对象 private Account account; private double drawMoney; public DrawThread(String name,Account account,double drawMoney){ //调用父类Thread构造方法，为线程命名 super(name); this.account=account; this.drawMoney=drawMoney; } @Override public void run(){ //若synchronized()加到run方法上，run方法仍然是一个并发线程 //run方法是一个特殊方法，是一个线程体方法 synchronized(this.account){ //判断当前账户余额 if(this.account.getBalance()>=this.drawMoney){ System.out.println(this.getName()+"取钱成功,"+this.drawMoney); //线程休眠 try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } this.account.setBalance(this.account.getBalance()-this.drawMoney); System.out.println("余额："+this.account.getBalance()); }else{ System.out.println(this.getName()+"余额不足"); } } } } public class DrawMoneyThread { public static void main(String[] args) { Account account=new Account("Jack",2000); new DrawThread("小明",account,1000).start(); new DrawThread("小花",account,1500).start(); } }打印结果：小明取钱成功,1000.0余额：1000.0小花余额不足线程同步的使用使用this作为线程对象锁在不同线程中，相同对象中的synchronized会互斥。语法结构：//-----法一----- synchronized(this){ //同步代码 } //-----法二----- public synchronized void f(){ //同步代码 }this作为线程对象锁案例class Programmer{ private String name; public Programmer(String name){ this.name=name; } //打开电脑方法 //synchronized也可写到 public之前 public void start(){ synchronized (this) { try { System.out.println(this.name + "接通电源"); Thread.sleep(500); System.out.println(this.name + "按开机键"); Thread.sleep(500); System.out.println(this.name + "系统启动"); Thread.sleep(500); System.out.println(this.name + "成功"); } catch (InterruptedException e) { throw new RuntimeException(e); } } } public void coding(){ synchronized (this) { try { System.out.println(this.name + "打开IDEA"); Thread.sleep(500); System.out.println(this.name + "IDEA打开成功"); Thread.sleep(500); System.out.println(this.name + "打开项目"); Thread.sleep(500); System.out.println(this.name + "工作"); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } //打开电脑的线程 class Working extends Thread{ private Programmer p; private String name; public Working(Programmer p){ this.p=p; } @Override public void run() { this.p.start(); } } //工作线程 class Working2 extends Thread{ private Programmer p; public Working2(Programmer p){ this.p=p; } @Override public void run() { this.p.coding(); } } public class MainApp { public static void main(String[] args) { Programmer p = new Programmer("小明"); Working w1=new Working(p); w1.start(); Working2 w2=new Working2(p); w2.start(); } }若不加synchronized锁，则会造成：小明接通电源小明打开IDEA小明IDEA打开成功小明按开机键小明打开项目小明系统启动小明工作小明成功加线程锁后：小明接通电源小明按开机键小明系统启动小明成功小明打开IDEA小明IDEA打开成功小明打开项目小明工作使用字符串作为线程对象锁字符串本质上是一个对象，如果使用字符串作为线程对象锁，所有线程在执行synchronized时都会同步。语法结构：synchronized("字符串"){ //同步代码 }字符串作为线程对象锁案例public class StudySyn { public static void main(String[] args) { Drink1 d1=new Drink1(new Programmer("张三")); Drink2 d2=new Drink2(new Programmer("李四")); d1.start(); d2.start(); } } class Programmer{ private String name; public Programmer(String name){ this.name=name; } public void start() { //利用字符串作为对象锁，可以填入任意字符串，甚至空串 synchronized ("线程锁") { try { System.out.println(name + "打开饮水机"); Thread.sleep(500); System.out.println(name + "放入杯子"); Thread.sleep(500); System.out.println(name + "按下出水按钮"); Thread.sleep(500); System.out.println(name + "拿走杯子"); Thread.sleep(500); System.out.println(name + "关上饮水机"); } catch (Exception e) { throw new RuntimeException(e); } } } } class Drink1 extends Thread{ private Programmer p; public Drink1(Programmer p){ this.p=p; } @Override public void run() { p.start(); } } class Drink2 extends Thread{ private Programmer p; public Drink2(Programmer p){ this.p=p; } @Override public void run() { p.start(); } }输出：张三打开饮水机张三放入杯子张三按下出水按钮张三拿走杯子张三关上饮水机李四打开饮水机李四放入杯子李四按下出水按钮李四拿走杯子李四关上饮水机使用Class作为线程对象锁Class是一个对象类型。当一个类被实例化时A a = new A()，JVM首先会使用类加载器（ClassLoader）加载到内存中，然后根据类创建一个Class类型的对象，最后在完成初始化动作等，JVM会将Class对象缓存下来，当以后再用到时就不需要重新加载了。在不同线程中，拥有相同Class对象中的synchronized会互斥。语法结构：synchronized(xx.class){ //同步代码 }或者：synchronized public static void accessVal()如果synchronized将静态方法作为对象锁，那么同步块在静态方法的{ }括号间；如果synchronized将非静态方法作为对象锁，那么以this作为对象锁。Class作为线程对象锁案例public class ClassSyn { public static void main(String[] args) { new Drink01(new Programmer("小明")).start(); new Drink02(new Programmer("小花")).start(); } } class Programmer{ private String name; public Programmer(String name){ this.name=name; } public void start(){ synchronized (Programmer.class) { try { System.out.println(name + "打开饮水机"); Thread.sleep(500); System.out.println(name + "放入杯子"); Thread.sleep(500); System.out.println(name + "按下出水按钮"); Thread.sleep(500); System.out.println(name + "拿走杯子"); Thread.sleep(500); System.out.println(name + "关上饮水机"); } catch (Exception e) { e.printStackTrace(); } } } } class Drink01 extends Thread{ private Programmer p; public Drink01(Programmer p){ this.p=p; } @Override public void run(){ p.start(); } } class Drink02 extends Thread{ private Programmer p; public Drink02(Programmer p){ this.p=p; } @Override public void run(){ p.start(); } }输出：小明打开饮水机小明放入杯子小明按下出水按钮小明拿走杯子小明关上饮水机小花打开饮水机小花放入杯子小花按下出水按钮小花拿走杯子小花关上饮水机使用自定义对象作为线程对象锁在不同线程中，拥有相同自定义对象中的synchronized会互斥。语法结构:synchronized(自定义对象){ //同步代码 }自定义对象作为线程对象锁案例老师上课随机点名，点到的同学答“到”，然后老师选择下一个同学点名。public class VoidSyn { public static void main(String[] args) { Teacher t=new Teacher("张三"); new TeacherThread(t,"小明").start(); new TeacherThread(t,"小花").start(); } } class Teacher{ private String name; public Teacher(String name){ this.name=name; } public void start(String sName){ //都是同一个Teacher运行方法，所以这里可以使用 this 实现线程同步 //synchronized (this) { try { System.out.println(this.name + "叫" + sName); Thread.sleep(500); System.out.println(sName + "听到老师叫自己"); Thread.sleep(500); System.out.println(sName + "答到"); Thread.sleep(500); System.out.println(this.name + "挑选下一个人"); } catch (Exception e) { e.printStackTrace(); } //} } } class TeacherThread extends Thread{ private Teacher t; private String sname; public TeacherThread(Teacher t,String sname){ this.t=t; this.sname=sname; } @Override public void run() { //传入的Teacher对象都是同一个，所以可以使用 this.t 实现线程同步 //synchronized (this.t) { t.start(sname); //} } }synchronized既可以放在线程对象中实现，也可以放到非线程对象中实现线程同步。死锁及解决方案死锁概念死锁是指两个或两个以上的进程在执行过程中，由于竞争资源或者由于彼此通信而造成的一种阻塞的现象，若无外力作用，它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁，这些永远在互相等待的进程称为死锁进程。死锁案例例如现在有两个 喝水 线程：线程1：小明想喝水（需要杯子和水桶）->拿到杯子->打开饮水机->接水线程2：小花想喝水（需要杯子和水桶）->拿到杯子->打开饮水机->接水但是杯子和水桶都只有一个，小明先去拿杯子花了0.5秒，回来发现小花已经拿到水桶了，此时小花没有水杯，有水桶；小明没有水桶，有水杯，都无法喝到水，而且两个人都在等对方喝完后把“资源”让出来，自己再喝。public class ClockSyn { public static void main(String[] args) { Cub c=new Cub(); Water w=new Water(); new GetWaterThread(new GetWater(),"小明",c,w).start(); new GetWaterThread2(new GetWater2(),"小花",c,w).start(); } } //水杯类 class Cub{ String name = "水杯"; } //水桶类 class Water{ String name = "水桶"; } class GetWater{ private Cub c; private Water w; public void start(String userName,Cub c,Water w){ try{ System.out.println(userName+"想喝水"); //小明拿杯子花了0.5s synchronized (c) { System.out.println(userName+"拿到"+c.name); Thread.sleep(500); synchronized (w) { System.out.println(userName + "拿着" + c.name + "打开" + w.name); Thread.sleep(500); System.out.println(userName + "用" + c.name + "接" + w.name + "的水"); Thread.sleep(500); System.out.println(userName + "喝完" + c.name + "里的水"); Thread.sleep(500); System.out.println(userName + "放回" + c.name); Thread.sleep(500); System.out.println(userName + "离开" + w.name); } } }catch (Exception e){ e.printStackTrace(); } } } class GetWater2{ private Cub c; private Water w; public void start(String userName,Cub c,Water w){ try{ System.out.println(userName+"想喝水"); //小花先拿到水桶 synchronized (w) { System.out.println(userName+"拿到"+w.name); synchronized (c) { System.out.println(userName + "拿着" + c.name + "打开" + w.name); Thread.sleep(500); System.out.println(userName + "用" + c.name + "接" + w.name + "的水"); Thread.sleep(500); System.out.println(userName + "喝完" + c.name + "里的水"); Thread.sleep(500); System.out.println(userName + "放回" + c.name); Thread.sleep(500); System.out.println(userName + "离开" + w.name); } } }catch (Exception e){ e.printStackTrace(); } } } //创建小明喝水线程 class GetWaterThread extends Thread{ private GetWater gw; private String userName; private Cub c; private Water w; public GetWaterThread(GetWater gw,String userName,Cub c,Water w){ this.gw=gw; this.userName=userName; this.c=c; this.w=w; } @Override public void run() { gw.start(userName,c,w); } } //创建小花喝水线程 class GetWaterThread2 extends Thread{ private GetWater2 gw; private String userName; private Cub c; private Water w; public GetWaterThread2(GetWater2 gw,String userName,Cub c,Water w){ this.gw=gw; this.userName=userName; this.c=c; this.w=w; } @Override public void run() { gw.start(userName,c,w); } }输出结果：小明想喝水小花想喝水小明拿到水杯小花拿到水桶两个人都无法喝到水。死锁解决方案保持良好编码习惯，编写同一个代码块时，不要同时持有两个对象锁。线程并发协作（生产者/消费者模式）当有多个线程的情况下，我们经常需要多个线程并发、协作。生产者：负责生产数据消费者：负责“消费”数据缓冲区：生产者将生产完的数据放入缓冲区，消费者再从缓冲区中拿取数据。缓冲区可以减少生产者和消费者之间的耦合，提高效率。生产者/ 消费者模式案例：public class CsPdSyn { public static void main(String[] args) { BufferStack bs=new BufferStack(); new Producer(bs).start(); new Customer(bs).start(); } } //生产者生产的“数据” class ManTou{ private int id; public ManTou(int id){ this.id=id; } } //缓冲区 class BufferStack{ //存放馒头的盒子 private ManTou[] mt=new ManTou[10]; //盒子内编号 private int index; //放入馒头方法 public synchronized void push(ManTou mt){ while(this.index==this.mt.length){ //wait() 会让当前线程进入阻塞状态，并且释放当前线程的对象锁 //所以该方法必须在synchronized块中使用 try { this.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } //避免两个线程都处于等待状态 //notify()必须在synchronized块中调用 //该方法会唤醒处于等待状态队列中的一个线程 this.notify(); this.mt[this.index]=mt; this.index++; } //取出馒头方法 public synchronized void pop(){ while(this.index==0){ try { this.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } this.notify(); this.mt[this.index]=null; this.index--; } } //生产者线程类 class Producer extends Thread{ private BufferStack bs; public Producer(BufferStack bs){ this.bs=bs; } @Override public void run() { for(int i=0;i<10;i++){ System.out.println("生产者生产了馒头："+i); ManTou mt=new ManTou(i); this.bs.push(mt); } } } //消费者线程类 class Customer extends Thread{ private BufferStack bs; public Customer(BufferStack bs){ this.bs=bs; } @Override public void run() { for(int i=0;i<10;i++){ this.bs.pop(); System.out.println("消费者消费了馒头："+i); } } }
什么是内部类？类的五大成员属性、方法、构造方法、代码块、内部类内部类即在一个类的里面，再定义一个类。内部类表示的事物是外部类的一部分内部类单独出现没有任何意义内部类访问特点内部类可以直接访问外部类成员，包括私有外部类要访问内部类的成员，必须创建对象内部类分类成员内部类静态内部类局部内部类匿名内部类成员内部类卸载成员位置的，属于外部类的成员成员内部类可以被一些修饰符所修饰，比如：private，默认，protected，public，static等在成员内部类里面，JDK16之前不能定义静态变量，JDK16以后才可以public class Car{ //外部类 String carName; int carAge; int carColor; class Engine{ //成员内部类 String engineName; int engineAge; } }获取成员内部类对象在外部类中编写方法，对外提供内部类的对象。直接创建格式：外部类名.内部类名 对象名 = 外部类对象.内部类对象public class Outer{ String name; class Inner{ //JDK16之后才可以这样书写 static int num = 10; } public Inner getInstance(){ return new Inner(); } }public class Test{ public static void main(String[] args){ //直接创建内部类对象 //若成员内部类被private修饰，则无法直接创建内部类对象，只能通过在外部类中编写方法，对外提供内部类的对象。 Outer.Inner oi = new Outer.new Inner(); //另一种方法 //若Inner私有，则不能使用 Outer.Inner inner = o.getInstance(); //可以使用Inner的父类创建，形成多态，但这里Inner没有父类，默认继承Object Object inner = o.getInstance(); //对象地址值 System.out.println(o.getinstance()); } }成员内部类如何获取外部类的成员变量class Outer{ private int a = 10; class Inner{ private int a = 20; public void show(){ int a = 30; //Outer.this 获取了外部类对象的地址值 System.out.println(Outer.this.a); //10 System.out.println(this.a); //20 System.out.println(a); //30 } } }Outer.this.a 的原理：image-20220922203031111静态内部类是成员内部类的一种特殊情况静态内部类只能访问外部类中的静态变量和静态方法，如果想要访问非静态的需要创建对象。public class Car{ //外部类 String carName; int carAge; int carColor; static class Engine{ //静态内部类 String engineName; int engineAge; } }获取静态内部类对象创建静态内部类对象的格式： 外部类名.内部类名 对象名 = new 外部类名.内部类名();public class Test{ public static void mian(String[] args){ //创建静态内部类的对象 //只要是静态的东西，都可以直接用类名点直接获取 //非静态方法 Outer.Inner oi = new Outer.Inner(); oi.show1(); //静态方法 Outer.Inner.show2(); } }public class outer{ int a = 10; static int b = 20; static class Inner{ public void show1(){ System.out.println(a); //报错 Outer o = new Outer(); System.out.println(o.a); //正常 System.out.println(b); //正常 System.out.println("非静态的方法被调用"); } public static void show2(){ System.out.peintln("静态方法被调用"); } } }局部内部类将内部类定义在方法里面就叫做局部内部类，类似于方法里面的局部变量。外界是无法直接使用，需要在方法内部创建对象并使用。该类可以直接访问外部类的成员，也可以访问方法内的局部变量。public class Test{ public static void main(String[] args){ /* //无法直接使用show方法里面的局部变量a Outer o = new Outer(); o.a; //这里a指成员变量，无法访问到show方法中的a */ Outer o = new Outer(); o.show(); //输出 /* null 0 10 20 局部内部类中的f1方法 局部内部类中的f2方法 */ } }public class Outer{ int b = 20; public void show(){ int a = 10; //局部内部类 //和局部变量类似 class Inner{ String name; int age; public void f1(){ System.out.println(a); //正常 System.out.println(b); //正常 System.out.println("局部内部类中的f1方法"); } public static void f2(){ System.out.println("局部内部类中的f2方法"); } } //创建局部内部类对象 Inner i = new Inner(); System.out.println(i.name); System.out.println(i.age); i.f1(); Inner.f2(); } }匿名内部类匿名内部类本质就是隐藏了名字的内部类格式：new 类名或直接接口名(){ 重写方法; };代码演示public class Test{ public static void main(String[] args){ //new 类名(); new Swim(){ @Override public void swim(){ System.out.println("游泳"); } }; } }public interface Swim{ public abstract void swim(); }//上文中代码段 { @Override public void swim(){ System.out.println("游泳"); }; //表示没有名字的类 //实现了Swim类，所以要重写Swim中的抽象方法 //若Swim为抽象类 //则是没有名字的类继承了Swim类，也要重写Swim中的抽象方法所以可以得出匿名内部类包含了：继承/实现关系；方法的重写；创建对象。匿名内部类作用public class Test{ public static void method(Animal a){ a.eat(); } //若要调用method方法 //需要先创建一个具体的动物类Dog Dog d = new Dog; method(d); //如果Dog只需要用一次，这种方法过于麻烦 //匿名内部类方法 method(new Animal(){ @Override public void eat(){ System.out.println("狗吃骨头"); } }); }public abstract class Animal{ public abstract eat(); }public class Dog extends Animal{ @Override public void eat(){ System.out.println("狗吃骨头"); } }
拼接时无变量String s = "a" + "b" + "c";拼接的时候没有变量，都是字符串。触发字符串的优化机制。在编译的时候就已经是最终的结果了。即当Java文件编译成字节码文件（ .class）时，会检查是否有变量参与，如果没有，则直接生成最终结果。上述代码经编译后的结果为：String s = "abc";拼接时有变量在JDK8以前String s1 = "a"; String s2 = s1 + "b"; String s3 = s2 + "c";当上述代码运行时：首先s1="a"，会再StringTable串池里生成一个字符串"a"，再将串池中字符串"a"的地址给到s1；s2=s1+"b",串池中没有字符串"b"，故需要在串池中生成一个字符串"b"，该表达式中有变量，所以Java会自动在内存中创建一个StringBuilder对象，然后通过append()方法把s1和"b"都放到对象中，然后再将StringBuilder对象通过toString()方法转换成String对戏。s3=s2+"c"，与上相同，会再创建一个StringBuilder对象，使用append()方法做拼接。整个过程相当于：new StringBuilder().append(s1).append("b").toString();通过分析StringBuiler的源码，可知字符串和变量一次拼接，内存中至少会有两个对象，一个是StringBuilder对象，一个是String对象。image-20220903160211455由此看出这样拼接，速度很慢，浪费内存。JDK8及以后String s1 = "a"; String s2 = "b"; String s3 = "c"; String s4 = s1 + s2 + s3;运行上述程序后，Java会根据s1、s2、s3预估s4的长度，然后创建一个数组，若为上述所示代码，会创建一个长度为3的数组，将"a","b","c"放入数组中，然后将数组变为一个字符串。但当有多个变量参与拼接时，预估最终字符串长度会耗费很多时间，在内存中也创建了很多对象，也会浪费空间。所以若有很多字符串变量拼接，不要直接+，而是使用StringBuilder或者StringJoiner。易混淆点String s1 = "abc"; String s2 = "ab"; String s3 = s2 + "c"; System.out.println(s1==s3);输出结果应为：false不管在JDK8之前还是在JDK8，字符串拼接时最终都会new一个String对象出来。JDK8之前：调用StringBuilder对象-->append()方法拼接-->toString方法（toString方法底层实际是new了一个String对象）JDK8：预估最终字符串长度-->将要拼接内容放入数组-->产生新的String对象String s1 = "abc"; String s2 = "a" + "b" + "c"; System.out.println(s1==s2);输出结果应为：trues2在编译时就已经被拼接完成了，所以s1，s2在串池中的地址值相同，输出true。